- 新增OCR文字识别模块:支持最新 PP-OCRv5

- OCR文本识别:支持文字方向检测与自动校正
This commit is contained in:
dengwenjie
2025-05-26 16:24:24 +08:00
parent 8dc5f83f47
commit 1914e2c316
59 changed files with 2523 additions and 892 deletions

View File

@@ -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 环境运行
- 引入离线依赖,支持完全离线使用

View File

@@ -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通用文字识别示例
```
## 快速开始

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
</encoder>
</appender>
<root level="INFO">
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
</root>
</configuration>

14
pom.xml
View File

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

View File

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

View File

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

View File

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

View File

@@ -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");
}
// 设置缓存路径的方法

View File

@@ -31,6 +31,8 @@ public class DetectionInfo {
*/
private ObjectDetInfo objectDetInfo;
public DetectionInfo() {
}

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ import lombok.Data;
public class FaceModelConfig {
/**
* 人脸算法名称
* 人脸模型枚举
*/
private FaceModelEnum modelEnum;

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import java.io.InputStream;
import java.util.List;
/**
* 人脸识别算法
* 人脸识别模型
* @author dwj
*/
public abstract class AbstractFaceModel implements FaceModel {

View File

@@ -10,7 +10,7 @@ import java.io.InputStream;
import java.util.List;
/**
* 人脸识别算法
* 人脸识别模型
* @author dwj
*/
public interface FaceModel {

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ import lombok.Data;
public class DetectorModelConfig {
/**
* 模型名称
* 模型
*/
private DetectorModelEnum modelEnum;

View File

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

View File

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

View File

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

View File

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

View File

@@ -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("默认不支持该功能");
}
}

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
package cn.smartjavaai.ocr.entity;
/**
* 身份证信息
* @author dwj
* @date 2025/5/22
*/
public class IdCardInfo {
}

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package cn.smartjavaai.ocr.entity;
import ai.djl.ndarray.NDArray;
/**
* 旋转检测框 - 支持左上角 X 坐标升序排序
*/

View File

@@ -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 + "°";
}
}

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
package cn.smartjavaai.ocr.exception;
/**
* 人脸检测异常
* OCR异常
* @author dwj
* @date 2025/4/4
*/

View File

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

View File

@@ -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("默认不支持该功能");
}
}

View File

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

View File

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

View File

@@ -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("默认不支持该功能");
}
}

View File

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

View File

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

View File

@@ -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("默认不支持该功能");
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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("默认不支持该功能");
}
}

View File

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

View File

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

View File

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

View File

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