mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-09-10 03:28:49 +00:00
- 新增OCR文字识别模块:支持最新 PP-OCRv5
- OCR文本识别:支持文字方向检测与自动校正
This commit is contained in:
40
README.md
40
README.md
@@ -135,7 +135,22 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/object_detection_detected.png" width = "500px"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div align="left">
|
||||
<p>OCR文字识别</p>
|
||||
- 支持任意角度文字识别 <br>
|
||||
- 支持印刷体识别 <br>
|
||||
- 支持手写字识别<br>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div align="center">
|
||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/general_ocr_002_recognized.png" width = "500px"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -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 开发者打造的一个功能丰富、开箱即用的
|
||||
<dependency>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-all</artifactId>
|
||||
<version>1.0.13</version>
|
||||
<version>1.0.14</version>
|
||||
</dependency>
|
||||
```
|
||||
### 3、完整示例代码
|
||||
@@ -277,6 +295,10 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
||||
|
||||
## 近期更新日志
|
||||
|
||||
## [v1.0.14] - 2025-05-17
|
||||
- 新增OCR文字识别模块:支持最新 PP-OCRv5
|
||||
- OCR文本识别:支持文字方向检测与自动校正
|
||||
|
||||
## [v1.0.13] - 2025-05-17
|
||||
- 支持 JDK8 环境运行
|
||||
- 引入离线依赖,支持完全离线使用
|
||||
|
||||
@@ -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通用文字识别示例
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<smartjavaai.version>1.0.13</smartjavaai.version>
|
||||
<smartjavaai.version>1.0.14</smartjavaai.version>
|
||||
<!--如果打包运行,需要替换成你的main-->
|
||||
<exec.mainClass>smartai.examples.face.facerec.RetinaFaceDemo</exec.mainClass>
|
||||
|
||||
@@ -103,6 +103,12 @@
|
||||
<artifactId>smartjavaai-objectdetection</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!--OCR检测模块-->
|
||||
<dependency>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-ocr</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>ai.djl.pytorch</groupId>
|
||||
|
||||
@@ -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<OcrBox> 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");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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<OcrItem> 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");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</root>
|
||||
</configuration>
|
||||
|
||||
14
pom.xml
14
pom.xml
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.13</version>
|
||||
<version>1.0.14</version>
|
||||
<packaging>pom</packaging>
|
||||
<description>SmartJavaAI</description>
|
||||
<modules>
|
||||
@@ -35,18 +35,6 @@
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-common</artifactId>
|
||||
<version>1.0.13</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-face</artifactId>
|
||||
<version>1.0.13</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</dependencyManagement>
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.13</version>
|
||||
<version>1.0.14</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-all</artifactId>
|
||||
<version>1.0.13</version>
|
||||
<version>1.0.14</version>
|
||||
<name>${project.artifactId}</name>
|
||||
<description>SmartJavaAI</description>
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.13</version>
|
||||
<version>1.0.14</version>
|
||||
</parent>
|
||||
|
||||
<version>1.0.13</version>
|
||||
<version>1.0.14</version>
|
||||
<artifactId>smartjavaai-bom</artifactId>
|
||||
<name>smartjavaai-bom</name>
|
||||
<description>统一版本管理的 BOM 包,同时支持 import 和全量依赖</description>
|
||||
@@ -33,6 +33,11 @@
|
||||
<artifactId>smartjavaai-objectdetection</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-ocr</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.13</version>
|
||||
<version>1.0.14</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-common</artifactId>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
// 设置缓存路径的方法
|
||||
|
||||
@@ -31,6 +31,8 @@ public class DetectionInfo {
|
||||
*/
|
||||
private ObjectDetInfo objectDetInfo;
|
||||
|
||||
|
||||
|
||||
public DetectionInfo() {
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.13</version>
|
||||
<version>1.0.14</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-face</artifactId>
|
||||
<version>1.0.13</version>
|
||||
<version>1.0.14</version>
|
||||
<name>smartjavaai-face</name>
|
||||
<description>SmartJavaAI</description>
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
|
||||
@@ -13,7 +13,7 @@ import lombok.Data;
|
||||
public class FaceModelConfig {
|
||||
|
||||
/**
|
||||
* 人脸算法名称
|
||||
* 人脸模型枚举
|
||||
*/
|
||||
private FaceModelEnum modelEnum;
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ public class FaceAttributeModelFactory {
|
||||
private static final ConcurrentHashMap<String, FaceAttributeModel> modelMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 算法注册表
|
||||
* 模型注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends FaceAttributeModel>> registry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
@@ -25,7 +25,7 @@ public class FaceModelFactory {
|
||||
private static final ConcurrentHashMap<String, FaceModel> modelMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 算法注册表
|
||||
* 模型注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends FaceModel>> 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
|
||||
*/
|
||||
|
||||
@@ -28,7 +28,7 @@ public class LivenessModelFactory {
|
||||
private static final ConcurrentHashMap<String, LivenessDetModel> modelMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 算法注册表
|
||||
* 模型注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends LivenessDetModel>> registry =
|
||||
new ConcurrentHashMap<>();
|
||||
@@ -72,7 +72,7 @@ public class LivenessModelFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用ModelConfig创建算法
|
||||
* 使用ModelConfig创建模型
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,7 @@ import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 人脸识别算法
|
||||
* 人脸识别模型
|
||||
* @author dwj
|
||||
*/
|
||||
public abstract class AbstractFaceModel implements FaceModel {
|
||||
|
||||
@@ -10,7 +10,7 @@ import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 人脸识别算法
|
||||
* 人脸识别模型
|
||||
* @author dwj
|
||||
*/
|
||||
public interface FaceModel {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.13</version>
|
||||
<version>1.0.14</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-objectdetection</artifactId>
|
||||
<version>1.0.13</version>
|
||||
<version>1.0.14</version>
|
||||
<name>smartjavaai-objectdetection</name>
|
||||
<description>SmartJavaAI</description>
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
|
||||
@@ -15,7 +15,7 @@ import lombok.Data;
|
||||
public class DetectorModelConfig {
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
* 模型
|
||||
*/
|
||||
private DetectorModelEnum modelEnum;
|
||||
|
||||
|
||||
@@ -6,18 +6,11 @@
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.13</version>
|
||||
<version>1.0.14</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-ocr</artifactId>
|
||||
|
||||
<properties>
|
||||
<!-- <maven.compiler.source>11</maven.compiler.source>-->
|
||||
<!-- <maven.compiler.target>11</maven.compiler.target>-->
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<javacv.version>1.5.8</javacv.version>
|
||||
<javacv.ffmpeg.version>5.1.2-1.5.8</javacv.ffmpeg.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
@@ -25,23 +18,9 @@
|
||||
<artifactId>smartjavaai-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bytedeco</groupId>
|
||||
<artifactId>javacpp</artifactId>
|
||||
<version>${javacv.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bytedeco</groupId>
|
||||
<artifactId>ffmpeg</artifactId>
|
||||
<version>${javacv.ffmpeg.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl.opencv</groupId>
|
||||
<artifactId>opencv</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<version>1.0.13</version>
|
||||
<version>1.0.14</version>
|
||||
<name>smartjavaai-ocr</name>
|
||||
<description>SmartJavaAI</description>
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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<String, OcrDetModel> modelMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 算法注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends OcrDetModel>> registry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public static OcrDetModelFactory getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (OcrDetModelFactory.class) {
|
||||
if (instance == null) {
|
||||
instance = new OcrDetModelFactory();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 注册算法
|
||||
* @param name
|
||||
* @param clazz
|
||||
*/
|
||||
private static void registerModel(String name, Class<? extends OcrDetModel> clazz) {
|
||||
registry.put(name.toLowerCase(), clazz);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public OcrDetModel getModel(OcrDetModelConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||
throw new OcrException("未配置OCR模型");
|
||||
}
|
||||
return modelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
|
||||
return createFaceModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用ModelConfig创建算法
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private OcrDetModel createFaceModel(OcrDetModelConfig config) {
|
||||
Class<?> clazz = registry.get(config.getModelEnum().name().toLowerCase());
|
||||
if(clazz == null){
|
||||
throw new OcrException("Unsupported model");
|
||||
}
|
||||
OcrDetModel algorithm = null;
|
||||
try {
|
||||
algorithm = (OcrDetModel) clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new OcrException(e);
|
||||
}
|
||||
algorithm.loadModel(config);
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
|
||||
// 初始化默认算法
|
||||
static {
|
||||
registerModel("PADDLEOCR_V4_DET_MODEL", PaddleOCRV4DetModel.class);
|
||||
log.info("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package cn.smartjavaai.ocr.entity;
|
||||
|
||||
/**
|
||||
* 身份证信息
|
||||
* @author dwj
|
||||
* @date 2025/5/22
|
||||
*/
|
||||
public class IdCardInfo {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<List<OcrItem>> lineList;
|
||||
|
||||
private String fullText;
|
||||
|
||||
|
||||
public OcrInfo(List<List<OcrItem>> lineList, String fullText) {
|
||||
this.lineList = lineList;
|
||||
this.fullText = fullText;
|
||||
}
|
||||
public OcrInfo() {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.smartjavaai.ocr.entity;
|
||||
|
||||
import ai.djl.ndarray.NDArray;
|
||||
|
||||
/**
|
||||
* 旋转检测框 - 支持左上角 X 坐标升序排序
|
||||
*/
|
||||
|
||||
@@ -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 + "°";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package cn.smartjavaai.ocr.exception;
|
||||
|
||||
/**
|
||||
* 人脸检测异常
|
||||
* OCR异常
|
||||
* @author dwj
|
||||
* @date 2025/4/4
|
||||
*/
|
||||
|
||||
@@ -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<String, OcrCommonDetModel> commonDetModelMap = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
private static final ConcurrentHashMap<String, OcrCommonRecModel> commonRecModelMap = new ConcurrentHashMap<>();
|
||||
|
||||
private static final ConcurrentHashMap<String, OcrDirectionModel> directionModelMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 检测模型注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends OcrCommonDetModel>> commonDetRegistry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 识别模型注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends OcrCommonRecModel>> commonRecRegistry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 方向分类模型注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends OcrDirectionModel>> directionRegistry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public static OcrModelFactory getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (OcrModelFactory.class) {
|
||||
if (instance == null) {
|
||||
instance = new OcrModelFactory();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 注册通用检测模型
|
||||
* @param name
|
||||
* @param clazz
|
||||
*/
|
||||
private static void registerCommonDetModel(String name, Class<? extends OcrCommonDetModel> clazz) {
|
||||
commonDetRegistry.put(name.toLowerCase(), clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册通用识别模型
|
||||
* @param name
|
||||
* @param clazz
|
||||
*/
|
||||
private static void registerCommonRecModel(String name, Class<? extends OcrCommonRecModel> clazz) {
|
||||
commonRecRegistry.put(name.toLowerCase(), clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册通用方向分类模型
|
||||
* @param name
|
||||
* @param clazz
|
||||
*/
|
||||
private static void registerDirectionModel(String name, Class<? extends OcrDirectionModel> clazz) {
|
||||
directionRegistry.put(name.toLowerCase(), clazz);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取检测模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public OcrCommonDetModel getDetModel(OcrDetModelConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||
throw new OcrException("未配置OCR模型");
|
||||
}
|
||||
return commonDetModelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
|
||||
return createCommonDetModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取识别模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public OcrCommonRecModel getRecModel(OcrRecModelConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getRecModelEnum())){
|
||||
throw new OcrException("未配置OCR模型");
|
||||
}
|
||||
return commonRecModelMap.computeIfAbsent(config.getRecModelEnum().name(), k -> {
|
||||
return createCommonRecModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public OcrDirectionModel getDirectionModel(DirectionModelConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||
throw new OcrException("未配置OCR模型");
|
||||
}
|
||||
return directionModelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
|
||||
return createDirectionModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 创建OCR通用检测模型
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private OcrCommonDetModel createCommonDetModel(OcrDetModelConfig config) {
|
||||
Class<?> clazz = commonDetRegistry.get(config.getModelEnum().name().toLowerCase());
|
||||
if(clazz == null){
|
||||
throw new OcrException("Unsupported model");
|
||||
}
|
||||
OcrCommonDetModel model = null;
|
||||
try {
|
||||
model = (OcrCommonDetModel) clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new OcrException(e);
|
||||
}
|
||||
model.loadModel(config);
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建OCR通用识别模型
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private OcrCommonRecModel createCommonRecModel(OcrRecModelConfig config) {
|
||||
Class<?> clazz = commonRecRegistry.get(config.getRecModelEnum().name().toLowerCase());
|
||||
if(clazz == null){
|
||||
throw new OcrException("Unsupported model");
|
||||
}
|
||||
OcrCommonRecModel model = null;
|
||||
try {
|
||||
model = (OcrCommonRecModel) clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new OcrException(e);
|
||||
}
|
||||
model.loadModel(config);
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建OCR方向分类模型
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private OcrDirectionModel createDirectionModel(DirectionModelConfig config) {
|
||||
Class<?> clazz = directionRegistry.get(config.getModelEnum().name().toLowerCase());
|
||||
if(clazz == null){
|
||||
throw new OcrException("Unsupported model");
|
||||
}
|
||||
OcrDirectionModel model = null;
|
||||
try {
|
||||
model = (OcrDirectionModel) clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new OcrException(e);
|
||||
}
|
||||
model.loadModel(config);
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
// 初始化默认算法
|
||||
static {
|
||||
registerCommonDetModel("PADDLEOCR_V5_DET_MODEL", PpOCRV5DetModel.class);
|
||||
registerCommonRecModel("PADDLEOCR_V5_REC_MODEL", PpOCRV5RecModel.class);
|
||||
registerDirectionModel("CH_PPOCR_MOBILE_V2_CLS", PPOCRMobileV2Model.class);
|
||||
log.info("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<OcrBox> detect(String imagePath) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 文本检测
|
||||
* @param image BufferedImage
|
||||
* @return
|
||||
*/
|
||||
default List<OcrBox> detect(BufferedImage image) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 文本检测
|
||||
* @param imageData 图片字节数组
|
||||
* @return
|
||||
*/
|
||||
default List<OcrBox> detect(byte[] imageData) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本检测
|
||||
* @param image DJL Image
|
||||
* @return
|
||||
*/
|
||||
default List<OcrBox> 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("默认不支持该功能");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Predictor<Image, NDList>> 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<Image, NDList> detCriteria =
|
||||
Criteria.builder()
|
||||
.optEngine("OnnxRuntime")
|
||||
.setTypes(Image.class, NDList.class)
|
||||
.optModelPath(Paths.get(config.getDetModelPath()))
|
||||
.optTranslator(new PPOCRV5DetTranslator(new ConcurrentHashMap<String, String>()))
|
||||
.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<OcrBox> 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<OcrBox> ocrBoxList = detect(img);
|
||||
((Mat)img.getWrappedImage()).release();
|
||||
return ocrBoxList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OcrBox> detect(Image image){
|
||||
Predictor<Image, NDList> 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<OcrBox> 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<OcrBox> detect(BufferedImage image) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new OcrException("图像无效");
|
||||
}
|
||||
Image img = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
|
||||
List<OcrBox> ocrBoxList = detect(img);
|
||||
((Mat)img.getWrappedImage()).release();
|
||||
return ocrBoxList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OcrBox> 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<OcrBox> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Image, NDList> {
|
||||
public class PPOCRV5DetTranslator implements Translator<Image, NDList> {
|
||||
// det_algorithm == "DB"
|
||||
private final float thresh = 0.3f;
|
||||
private final boolean use_dilation = false;
|
||||
@@ -44,7 +44,7 @@ public class PaddleOCRV4DetectTranslator implements Translator<Image, NDList> {
|
||||
private int img_height;
|
||||
private int img_width;
|
||||
|
||||
public PaddleOCRV4DetectTranslator(Map<String, ?> arguments) {
|
||||
public PPOCRV5DetTranslator(Map<String, ?> arguments) {
|
||||
limit_side_len =
|
||||
arguments.containsKey("limit_side_len")
|
||||
? Integer.parseInt(arguments.get("limit_side_len").toString())
|
||||
@@ -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<OcrItem> detect(String imagePath) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 文本方向检测
|
||||
* @param image BufferedImage
|
||||
* @return
|
||||
*/
|
||||
default List<OcrItem> detect(BufferedImage image) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 文本方向检测
|
||||
* @param imageData 图片字节数组
|
||||
* @return
|
||||
*/
|
||||
default List<OcrItem> detect(byte[] imageData) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本方向检测
|
||||
* @param image
|
||||
* @return
|
||||
*/
|
||||
default List<OcrItem> detect(Image image) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 文本方向检测(基于检测结果)
|
||||
* @param boxList
|
||||
* @param srcMat
|
||||
* @param manager
|
||||
* @return
|
||||
*/
|
||||
default List<OcrItem> detect(List<OcrBox> 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("默认不支持该功能");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Predictor<Image, DirectionInfo>> 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<Image, DirectionInfo> 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<OcrItem> 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<OcrItem> ocrItemList = detect(img);
|
||||
((Mat)img.getWrappedImage()).release();
|
||||
return ocrItemList;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<OcrItem> detect(Image image){
|
||||
//检测文本
|
||||
List<OcrBox> boxeList = detModel.detect(image);
|
||||
if(Objects.isNull(boxeList) || boxeList.isEmpty()){
|
||||
throw new OcrException("未检测到文本");
|
||||
}
|
||||
Predictor<Image, DirectionInfo> predictor = null;
|
||||
List<OcrItem> 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<Image, DirectionInfo> 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<OcrItem> detect(List<OcrBox> boxList,Mat srcMat,NDManager manager){
|
||||
if(Objects.isNull(boxList) || boxList.isEmpty()){
|
||||
throw new OcrException("boxList为空");
|
||||
}
|
||||
Predictor<Image, DirectionInfo> predictor = null;
|
||||
List<OcrItem> 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<OcrItem> 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<OcrItem> detect(BufferedImage image) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new OcrException("图像无效");
|
||||
}
|
||||
Image img = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
|
||||
List<OcrItem> ocrItemList = detect(img);
|
||||
((Mat)img.getWrappedImage()).release();
|
||||
return ocrItemList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OcrItem> 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<OcrItem> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Image, DirectionInfo> {
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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("默认不支持该功能");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Predictor<Image, String>> 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<Image, String> recCriteria =
|
||||
Criteria.builder()
|
||||
.optEngine("OnnxRuntime")
|
||||
.setTypes(Image.class, String.class)
|
||||
.optModelPath(Paths.get(config.getRecModelPath()))
|
||||
.optTranslator(new PPOCRV5RecTranslator(new ConcurrentHashMap<String, String>()))
|
||||
.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<OcrBox> boxeList = detModel.detect(image);
|
||||
if(Objects.isNull(boxeList) || boxeList.isEmpty()){
|
||||
throw new OcrException("未检测到文本");
|
||||
}
|
||||
Predictor<Image, String> predictor = null;
|
||||
List<RotatedBox> rotatedBoxes = new ArrayList<>();
|
||||
List<OcrItem> 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<Image, String> 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<RotatedBox> rotatedBoxes){
|
||||
//Y坐标升序排序
|
||||
List<RotatedBox> initList = new ArrayList<>();
|
||||
for (RotatedBox result : rotatedBoxes) {
|
||||
initList.add(result);
|
||||
}
|
||||
Collections.sort(initList);
|
||||
//多行文本框的集合
|
||||
List<ArrayList<RotatedBoxCompX>> lines = new ArrayList<>();
|
||||
List<RotatedBoxCompX> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Image, String> {
|
||||
public class PPOCRV5RecTranslator implements Translator<Image, String> {
|
||||
private List<String> table;
|
||||
private final boolean use_space_char;
|
||||
|
||||
public PaddleOCRV4WordRecTranslator(Map<String, ?> arguments) {
|
||||
public PPOCRV5RecTranslator(Map<String, ?> arguments) {
|
||||
use_space_char =
|
||||
arguments.containsKey("use_space_char")
|
||||
? Boolean.parseBoolean(arguments.get("use_space_char").toString())
|
||||
@@ -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<Predictor<Image, NDList>> predictorPool;
|
||||
|
||||
@Override
|
||||
public void loadModel(OcrDetModelConfig config){
|
||||
if(StringUtils.isBlank(config.getModelPath())){
|
||||
throw new OcrException("modelPath is null");
|
||||
}
|
||||
Criteria<Image, NDList> criteria =
|
||||
Criteria.builder()
|
||||
.optEngine("OnnxRuntime")
|
||||
.setTypes(Image.class, NDList.class)
|
||||
.optModelPath(Paths.get(config.getModelPath()))
|
||||
.optTranslator(new PaddleOCRV4DetectTranslator(new ConcurrentHashMap<String, String>()))
|
||||
.optProgress(new ProgressBar())
|
||||
.build();
|
||||
try{
|
||||
detectionModel = ModelZoo.loadModel(criteria);
|
||||
// 创建池子:每个线程独享 Predictor
|
||||
this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(detectionModel));
|
||||
log.info("当前设备: " + detectionModel.getNDManager().getDevice());
|
||||
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
|
||||
throw new OcrException("模型加载失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DetectionResponse detect(String imagePath){
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new OcrException("图像文件不存在");
|
||||
}
|
||||
Image img = null;
|
||||
try {
|
||||
img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
} catch (IOException e) {
|
||||
throw new OcrException("无效的图片", e);
|
||||
}
|
||||
return detect(img);
|
||||
}
|
||||
|
||||
private DetectionResponse detect(Image image){
|
||||
Predictor<Image, NDList> predictor = null;
|
||||
try {
|
||||
predictor = predictorPool.borrowObject();
|
||||
NDList result = predictor.predict(image);
|
||||
return OcrUtils.convertToDetectionResponse(result, image);
|
||||
} catch (Exception e) {
|
||||
throw new OcrException("OCR检测错误", e);
|
||||
}finally {
|
||||
if (predictor != null) {
|
||||
try {
|
||||
predictorPool.returnObject(predictor); //归还
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
try {
|
||||
predictor.close(); // 归还失败才销毁
|
||||
} catch (Exception ex) {
|
||||
log.error("关闭Predictor失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detectAndDraw(String imagePath, String outputPath) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new OcrException("图像文件不存在");
|
||||
}
|
||||
try {
|
||||
Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
DetectionResponse detectionResponse = detect(img);
|
||||
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Predictor<Image, NDList>> predictorPool;
|
||||
|
||||
@Override
|
||||
public void loadModel(OcrRecModelConfig config){
|
||||
if(StringUtils.isBlank(config.getModelPath())){
|
||||
throw new OcrException("modelPath is null");
|
||||
}
|
||||
Criteria<Image, NDList> criteria =
|
||||
Criteria.builder()
|
||||
.optEngine("OnnxRuntime")
|
||||
.setTypes(Image.class, NDList.class)
|
||||
.optModelPath(Paths.get(config.getModelPath()))
|
||||
.optTranslator(new PaddleOCRV4DetectTranslator(new ConcurrentHashMap<String, String>()))
|
||||
.optProgress(new ProgressBar())
|
||||
.build();
|
||||
try{
|
||||
detectionModel = ModelZoo.loadModel(criteria);
|
||||
// 创建池子:每个线程独享 Predictor
|
||||
this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(detectionModel));
|
||||
log.info("当前设备: " + detectionModel.getNDManager().getDevice());
|
||||
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
|
||||
throw new OcrException("模型加载失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DetectionResponse detect(String imagePath){
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new OcrException("图像文件不存在");
|
||||
}
|
||||
Image img = null;
|
||||
try {
|
||||
img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
} catch (IOException e) {
|
||||
throw new OcrException("无效的图片", e);
|
||||
}
|
||||
return detect(img);
|
||||
}
|
||||
|
||||
private DetectionResponse detect(Image image){
|
||||
Predictor<Image, NDList> predictor = null;
|
||||
try {
|
||||
predictor = predictorPool.borrowObject();
|
||||
NDList result = predictor.predict(image);
|
||||
return OcrUtils.convertToDetectionResponse(result, image);
|
||||
} catch (Exception e) {
|
||||
throw new OcrException("OCR检测错误", e);
|
||||
}finally {
|
||||
if (predictor != null) {
|
||||
try {
|
||||
predictorPool.returnObject(predictor); //归还
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
try {
|
||||
predictor.close(); // 归还失败才销毁
|
||||
} catch (Exception ex) {
|
||||
log.error("关闭Predictor失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detectAndDraw(String imagePath, String outputPath) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new OcrException("图像文件不存在");
|
||||
}
|
||||
try {
|
||||
Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
DetectionResponse detectionResponse = detect(img);
|
||||
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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<String, OcrRecModel> modelMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 算法注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends OcrRecModel>> registry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public static OcrRecModelFactory getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (OcrRecModelFactory.class) {
|
||||
if (instance == null) {
|
||||
instance = new OcrRecModelFactory();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 注册算法
|
||||
* @param name
|
||||
* @param clazz
|
||||
*/
|
||||
private static void registerModel(String name, Class<? extends OcrRecModel> clazz) {
|
||||
registry.put(name.toLowerCase(), clazz);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public OcrRecModel getModel(OcrRecModelConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||
throw new OcrException("未配置OCR模型");
|
||||
}
|
||||
return modelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
|
||||
return createFaceModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用ModelConfig创建算法
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private OcrRecModel createFaceModel(OcrRecModelConfig config) {
|
||||
Class<?> clazz = registry.get(config.getModelEnum().name().toLowerCase());
|
||||
if(clazz == null){
|
||||
throw new OcrException("Unsupported model");
|
||||
}
|
||||
OcrRecModel algorithm = null;
|
||||
try {
|
||||
algorithm = (OcrRecModel) clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new OcrException(e);
|
||||
}
|
||||
algorithm.loadModel(config);
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
|
||||
// 初始化默认算法
|
||||
static {
|
||||
//registerModel("PADDLEOCR_V4_REC_MODEL", PaddleOCRV4DetModel.class);
|
||||
log.info("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<OcrBox> convertToOcrBox(NDList dt_boxes, Image img){
|
||||
if(Objects.isNull(dt_boxes) || dt_boxes.size() == 0){
|
||||
return null;
|
||||
}
|
||||
DetectionResponse detectionResponse = new DetectionResponse();
|
||||
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
|
||||
List<OcrBox> boxList = new ArrayList<OcrBox>();
|
||||
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<ArrayList<RotatedBoxCompX>> lines){
|
||||
if(Objects.isNull(lines) || lines.size() == 0){
|
||||
return null;
|
||||
}
|
||||
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
|
||||
List<List<OcrItem>> lineList = new ArrayList<List<OcrItem>>();
|
||||
String fullText = "";
|
||||
for(ArrayList<RotatedBoxCompX> boxList : lines){
|
||||
List<OcrItem> line = new ArrayList<OcrItem>();
|
||||
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<ai.djl.modality.cv.output.Point> 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<ai.djl.modality.cv.output.Point> 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<OcrBox> 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<OcrItem> 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<OcrItem> 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user