mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-09-12 20:58:51 +00:00
- 新增OCR文字识别模块:支持最新 PP-OCRv5
- OCR文本识别:支持文字方向检测与自动校正
This commit is contained in:
@@ -6,18 +6,11 @@
|
||||
<parent>
|
||||
<groupId>cn.smartjavaai</groupId>
|
||||
<artifactId>smartjavaai-parent</artifactId>
|
||||
<version>1.0.13</version>
|
||||
<version>1.0.14</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>smartjavaai-ocr</artifactId>
|
||||
|
||||
<properties>
|
||||
<!-- <maven.compiler.source>11</maven.compiler.source>-->
|
||||
<!-- <maven.compiler.target>11</maven.compiler.target>-->
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<javacv.version>1.5.8</javacv.version>
|
||||
<javacv.ffmpeg.version>5.1.2-1.5.8</javacv.ffmpeg.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
@@ -25,23 +18,9 @@
|
||||
<artifactId>smartjavaai-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bytedeco</groupId>
|
||||
<artifactId>javacpp</artifactId>
|
||||
<version>${javacv.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bytedeco</groupId>
|
||||
<artifactId>ffmpeg</artifactId>
|
||||
<version>${javacv.ffmpeg.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.djl.opencv</groupId>
|
||||
<artifactId>opencv</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<version>1.0.13</version>
|
||||
<version>1.0.14</version>
|
||||
<name>smartjavaai-ocr</name>
|
||||
<description>SmartJavaAI</description>
|
||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package cn.smartjavaai.ocr.config;
|
||||
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.ocr.enums.CommonDetModelEnum;
|
||||
import cn.smartjavaai.ocr.enums.DirectionModelEnum;
|
||||
import cn.smartjavaai.ocr.model.common.recognize.OcrCommonRecModel;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 文本方向分类模型配置
|
||||
* @author dwj
|
||||
* @date 2025/4/22
|
||||
*/
|
||||
@Data
|
||||
public class DirectionModelConfig {
|
||||
|
||||
/**
|
||||
* 模型
|
||||
*/
|
||||
private DirectionModelEnum modelEnum;
|
||||
|
||||
/**
|
||||
* 设备类型
|
||||
*/
|
||||
private DeviceEnum device;
|
||||
|
||||
/**
|
||||
* 检测模型路径
|
||||
*/
|
||||
private String modelPath;
|
||||
|
||||
/**
|
||||
* 检测模型
|
||||
*/
|
||||
private CommonDetModelEnum detModelEnum;
|
||||
|
||||
/**
|
||||
* 检测模型路径
|
||||
*/
|
||||
private String detModelPath;
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package cn.smartjavaai.ocr.detection;
|
||||
package cn.smartjavaai.ocr.config;
|
||||
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.ocr.enums.CommonDetModelEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* OCR检测模型配置
|
||||
* @author dwj
|
||||
* @date 2025/4/22
|
||||
*/
|
||||
@@ -11,9 +13,9 @@ import lombok.Data;
|
||||
public class OcrDetModelConfig {
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
* 模型
|
||||
*/
|
||||
private OcrDetModelEnum modelEnum;
|
||||
private CommonDetModelEnum modelEnum;
|
||||
|
||||
/**
|
||||
* 设备类型
|
||||
@@ -21,8 +23,9 @@ public class OcrDetModelConfig {
|
||||
private DeviceEnum device;
|
||||
|
||||
/**
|
||||
* 模型路径
|
||||
* 检测模型路径
|
||||
*/
|
||||
private String modelPath;
|
||||
private String detModelPath;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package cn.smartjavaai.ocr.config;
|
||||
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.ocr.enums.CommonDetModelEnum;
|
||||
import cn.smartjavaai.ocr.enums.CommonRecModelEnum;
|
||||
import cn.smartjavaai.ocr.enums.DirectionModelEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* OCR识别模型配置
|
||||
* @author dwj
|
||||
* @date 2025/4/22
|
||||
*/
|
||||
@Data
|
||||
public class OcrRecModelConfig {
|
||||
|
||||
/**
|
||||
* 检测模型
|
||||
*/
|
||||
private CommonDetModelEnum detModelEnum;
|
||||
|
||||
/**
|
||||
* 识别模型
|
||||
*/
|
||||
private CommonRecModelEnum recModelEnum;
|
||||
|
||||
/**
|
||||
* 设备类型
|
||||
*/
|
||||
private DeviceEnum device;
|
||||
|
||||
/**
|
||||
* 检测模型路径
|
||||
*/
|
||||
private String detModelPath;
|
||||
|
||||
/**
|
||||
* 识别模型路径
|
||||
*/
|
||||
private String recModelPath;
|
||||
|
||||
/**
|
||||
* 方向检测模型
|
||||
*/
|
||||
private DirectionModelEnum directionModelEnum;
|
||||
|
||||
/**
|
||||
* 方向检测模型路径
|
||||
*/
|
||||
private String directionModelPath;
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package cn.smartjavaai.ocr.detection;
|
||||
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
|
||||
/**
|
||||
* 人脸识别算法
|
||||
* @author dwj
|
||||
*/
|
||||
public interface OcrDetModel {
|
||||
|
||||
/**
|
||||
* 加载模型
|
||||
* @param config
|
||||
*/
|
||||
void loadModel(OcrDetModelConfig config); // 加载模型
|
||||
|
||||
/**
|
||||
* 人脸检测
|
||||
* @param imagePath 图片路径
|
||||
* @return
|
||||
*/
|
||||
default DetectionResponse detect(String imagePath) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测并绘制结果
|
||||
* @param imagePath 图片输入路径(包含文件名称)
|
||||
* @param outputPath 图片输出路径(包含文件名称)
|
||||
*/
|
||||
default void detectAndDraw(String imagePath, String outputPath) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package cn.smartjavaai.ocr.detection;
|
||||
|
||||
import cn.smartjavaai.common.config.Config;
|
||||
import cn.smartjavaai.ocr.exception.OcrException;
|
||||
import cn.smartjavaai.ocr.ppv4.model.PaddleOCRV4DetModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 人脸算法工厂
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class OcrDetModelFactory {
|
||||
|
||||
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
|
||||
private static volatile OcrDetModelFactory instance;
|
||||
|
||||
private static final ConcurrentHashMap<String, OcrDetModel> modelMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 算法注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends OcrDetModel>> registry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public static OcrDetModelFactory getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (OcrDetModelFactory.class) {
|
||||
if (instance == null) {
|
||||
instance = new OcrDetModelFactory();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 注册算法
|
||||
* @param name
|
||||
* @param clazz
|
||||
*/
|
||||
private static void registerModel(String name, Class<? extends OcrDetModel> clazz) {
|
||||
registry.put(name.toLowerCase(), clazz);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public OcrDetModel getModel(OcrDetModelConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||
throw new OcrException("未配置OCR模型");
|
||||
}
|
||||
return modelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
|
||||
return createFaceModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用ModelConfig创建算法
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private OcrDetModel createFaceModel(OcrDetModelConfig config) {
|
||||
Class<?> clazz = registry.get(config.getModelEnum().name().toLowerCase());
|
||||
if(clazz == null){
|
||||
throw new OcrException("Unsupported model");
|
||||
}
|
||||
OcrDetModel algorithm = null;
|
||||
try {
|
||||
algorithm = (OcrDetModel) clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new OcrException(e);
|
||||
}
|
||||
algorithm.loadModel(config);
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
|
||||
// 初始化默认算法
|
||||
static {
|
||||
registerModel("PADDLEOCR_V4_DET_MODEL", PaddleOCRV4DetModel.class);
|
||||
log.info("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package cn.smartjavaai.ocr.entity;
|
||||
|
||||
/**
|
||||
* 方向检测结果
|
||||
* @author Calvin
|
||||
* @mail 179209347@qq.com
|
||||
* @website www.aias.top
|
||||
*/
|
||||
public class DirectionInfo {
|
||||
|
||||
/**
|
||||
* 方向 0 90 180 270
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 置信度
|
||||
*/
|
||||
private Double prob;
|
||||
|
||||
public DirectionInfo(String name, Double prob) {
|
||||
this.name = name;
|
||||
this.prob = prob;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Double getProb() {
|
||||
return prob;
|
||||
}
|
||||
|
||||
public void setProb(Double prob) {
|
||||
this.prob = prob;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package cn.smartjavaai.ocr.entity;
|
||||
|
||||
/**
|
||||
* 身份证信息
|
||||
* @author dwj
|
||||
* @date 2025/5/22
|
||||
*/
|
||||
public class IdCardInfo {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package cn.smartjavaai.ocr.entity;
|
||||
|
||||
import cn.smartjavaai.common.entity.Point;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* OCR 检测框
|
||||
* @author dwj
|
||||
* @date 2025/5/20
|
||||
*/
|
||||
@Data
|
||||
public class OcrBox {
|
||||
|
||||
/**
|
||||
* 左上角
|
||||
*/
|
||||
private Point topLeft;
|
||||
|
||||
/**
|
||||
* 右上角
|
||||
*/
|
||||
private Point topRight;
|
||||
|
||||
/**
|
||||
* 右下角
|
||||
*/
|
||||
private Point bottomRight;
|
||||
|
||||
/**
|
||||
* 左下角
|
||||
*/
|
||||
private Point bottomLeft;
|
||||
|
||||
public OcrBox(Point topLeft, Point topRight, Point bottomRight, Point bottomLeft) {
|
||||
this.topLeft = topLeft;
|
||||
this.topRight = topRight;
|
||||
this.bottomRight = bottomRight;
|
||||
this.bottomLeft = bottomLeft;
|
||||
}
|
||||
|
||||
public OcrBox() {
|
||||
}
|
||||
|
||||
public float[] toFloatArray() {
|
||||
return new float[]{
|
||||
(float)topLeft.getX(), (float)topLeft.getY(),
|
||||
(float)topRight.getX(), (float)topRight.getY(),
|
||||
(float)bottomRight.getX(), (float)bottomRight.getY(),
|
||||
(float)bottomLeft.getX(), (float)bottomLeft.getY()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cn.smartjavaai.ocr.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* OCR信息
|
||||
* @author dwj
|
||||
* @date 2025/5/20
|
||||
*/
|
||||
@Data
|
||||
public class OcrInfo {
|
||||
|
||||
private List<List<OcrItem>> lineList;
|
||||
|
||||
private String fullText;
|
||||
|
||||
|
||||
public OcrInfo(List<List<OcrItem>> lineList, String fullText) {
|
||||
this.lineList = lineList;
|
||||
this.fullText = fullText;
|
||||
}
|
||||
public OcrInfo() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package cn.smartjavaai.ocr.entity;
|
||||
|
||||
import cn.smartjavaai.ocr.enums.AngleEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
* @date 2025/5/20
|
||||
*/
|
||||
@Data
|
||||
public class OcrItem {
|
||||
|
||||
/**
|
||||
* 识别框
|
||||
*/
|
||||
private OcrBox ocrBox;
|
||||
|
||||
/**
|
||||
* 文本
|
||||
*/
|
||||
private String text;
|
||||
|
||||
/**
|
||||
* 方向
|
||||
*/
|
||||
private AngleEnum angle;
|
||||
|
||||
/**
|
||||
* 检测得分
|
||||
*/
|
||||
private float score;
|
||||
|
||||
|
||||
|
||||
public OcrItem(OcrBox ocrBox, String text) {
|
||||
this.ocrBox = ocrBox;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public OcrItem() {
|
||||
}
|
||||
|
||||
public OcrItem(OcrBox ocrBox, String text, AngleEnum angle) {
|
||||
this.ocrBox = ocrBox;
|
||||
this.text = text;
|
||||
this.angle = angle;
|
||||
}
|
||||
|
||||
public OcrItem(OcrBox ocrBox, AngleEnum angle, float score) {
|
||||
this.ocrBox = ocrBox;
|
||||
this.angle = angle;
|
||||
this.score = score;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.smartjavaai.ocr.entity;
|
||||
|
||||
import ai.djl.ndarray.NDArray;
|
||||
|
||||
/**
|
||||
* 旋转检测框 - 支持左上角 X 坐标升序排序
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.smartjavaai.ocr.enums;
|
||||
|
||||
/**
|
||||
* 文本方向
|
||||
* @author dwj
|
||||
* @date 2025/5/23
|
||||
*/
|
||||
public enum AngleEnum {
|
||||
|
||||
ANGLE_0("0"),
|
||||
ANGLE_90("90"),
|
||||
ANGLE_180("180"),
|
||||
ANGLE_270("270");
|
||||
|
||||
private final String value;
|
||||
|
||||
AngleEnum(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static AngleEnum fromValue(String value) {
|
||||
for (AngleEnum angle : values()) {
|
||||
if (angle.value.equals(value)) {
|
||||
return angle;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Invalid angle value: " + value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value + "°";
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
package cn.smartjavaai.ocr.detection;
|
||||
package cn.smartjavaai.ocr.enums;
|
||||
|
||||
/**
|
||||
* OCR模型枚举
|
||||
* OCR检测模型枚举
|
||||
* @author dwj
|
||||
* @date 2025/4/4
|
||||
*/
|
||||
public enum OcrDetModelEnum {
|
||||
public enum CommonDetModelEnum {
|
||||
|
||||
PADDLEOCR_V4_DET_MODEL;
|
||||
PADDLEOCR_V5_DET_MODEL;
|
||||
|
||||
|
||||
/**
|
||||
* 根据名称获取枚举 (忽略大小写和下划线变体)
|
||||
*/
|
||||
public static OcrDetModelEnum fromName(String name) {
|
||||
public static CommonDetModelEnum fromName(String name) {
|
||||
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
|
||||
for (OcrDetModelEnum model : values()) {
|
||||
for (CommonDetModelEnum model : values()) {
|
||||
if (model.name().replaceAll("_", "").equals(formatted)) {
|
||||
return model;
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
package cn.smartjavaai.ocr.recognition;
|
||||
package cn.smartjavaai.ocr.enums;
|
||||
|
||||
/**
|
||||
* OCR识别模型枚举
|
||||
* @author dwj
|
||||
* @date 2025/4/4
|
||||
*/
|
||||
public enum OcrRecModelEnum {
|
||||
public enum CommonRecModelEnum {
|
||||
|
||||
PADDLEOCR_V4_REC_MODEL;
|
||||
PADDLEOCR_V5_REC_MODEL;
|
||||
|
||||
|
||||
/**
|
||||
* 根据名称获取枚举 (忽略大小写和下划线变体)
|
||||
*/
|
||||
public static OcrRecModelEnum fromName(String name) {
|
||||
public static CommonRecModelEnum fromName(String name) {
|
||||
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
|
||||
for (OcrRecModelEnum model : values()) {
|
||||
for (CommonRecModelEnum model : values()) {
|
||||
if (model.name().replaceAll("_", "").equals(formatted)) {
|
||||
return model;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cn.smartjavaai.ocr.enums;
|
||||
|
||||
/**
|
||||
* OCR文本方向分类模型枚举
|
||||
* @author dwj
|
||||
* @date 2025/4/4
|
||||
*/
|
||||
public enum DirectionModelEnum {
|
||||
|
||||
CH_PPOCR_MOBILE_V2_CLS;
|
||||
|
||||
|
||||
/**
|
||||
* 根据名称获取枚举 (忽略大小写和下划线变体)
|
||||
*/
|
||||
public static DirectionModelEnum fromName(String name) {
|
||||
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
|
||||
for (DirectionModelEnum model : values()) {
|
||||
if (model.name().replaceAll("_", "").equals(formatted)) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("未知模型名称: " + name);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package cn.smartjavaai.ocr.exception;
|
||||
|
||||
/**
|
||||
* 人脸检测异常
|
||||
* OCR异常
|
||||
* @author dwj
|
||||
* @date 2025/4/4
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
package cn.smartjavaai.ocr.factory;
|
||||
|
||||
import cn.smartjavaai.common.config.Config;
|
||||
import cn.smartjavaai.ocr.config.DirectionModelConfig;
|
||||
import cn.smartjavaai.ocr.config.OcrRecModelConfig;
|
||||
import cn.smartjavaai.ocr.model.common.detect.OcrCommonDetModel;
|
||||
import cn.smartjavaai.ocr.model.common.detect.PpOCRV5DetModel;
|
||||
import cn.smartjavaai.ocr.model.common.recognize.OcrCommonRecModel;
|
||||
import cn.smartjavaai.ocr.config.OcrDetModelConfig;
|
||||
import cn.smartjavaai.ocr.exception.OcrException;
|
||||
import cn.smartjavaai.ocr.model.common.direction.OcrDirectionModel;
|
||||
import cn.smartjavaai.ocr.model.common.direction.PPOCRMobileV2Model;
|
||||
import cn.smartjavaai.ocr.model.common.recognize.PpOCRV5RecModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* OCR模型工厂
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class OcrModelFactory {
|
||||
|
||||
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
|
||||
private static volatile OcrModelFactory instance;
|
||||
|
||||
private static final ConcurrentHashMap<String, OcrCommonDetModel> commonDetModelMap = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
private static final ConcurrentHashMap<String, OcrCommonRecModel> commonRecModelMap = new ConcurrentHashMap<>();
|
||||
|
||||
private static final ConcurrentHashMap<String, OcrDirectionModel> directionModelMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 检测模型注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends OcrCommonDetModel>> commonDetRegistry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 识别模型注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends OcrCommonRecModel>> commonRecRegistry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 方向分类模型注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends OcrDirectionModel>> directionRegistry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public static OcrModelFactory getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (OcrModelFactory.class) {
|
||||
if (instance == null) {
|
||||
instance = new OcrModelFactory();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 注册通用检测模型
|
||||
* @param name
|
||||
* @param clazz
|
||||
*/
|
||||
private static void registerCommonDetModel(String name, Class<? extends OcrCommonDetModel> clazz) {
|
||||
commonDetRegistry.put(name.toLowerCase(), clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册通用识别模型
|
||||
* @param name
|
||||
* @param clazz
|
||||
*/
|
||||
private static void registerCommonRecModel(String name, Class<? extends OcrCommonRecModel> clazz) {
|
||||
commonRecRegistry.put(name.toLowerCase(), clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册通用方向分类模型
|
||||
* @param name
|
||||
* @param clazz
|
||||
*/
|
||||
private static void registerDirectionModel(String name, Class<? extends OcrDirectionModel> clazz) {
|
||||
directionRegistry.put(name.toLowerCase(), clazz);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取检测模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public OcrCommonDetModel getDetModel(OcrDetModelConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||
throw new OcrException("未配置OCR模型");
|
||||
}
|
||||
return commonDetModelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
|
||||
return createCommonDetModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取识别模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public OcrCommonRecModel getRecModel(OcrRecModelConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getRecModelEnum())){
|
||||
throw new OcrException("未配置OCR模型");
|
||||
}
|
||||
return commonRecModelMap.computeIfAbsent(config.getRecModelEnum().name(), k -> {
|
||||
return createCommonRecModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public OcrDirectionModel getDirectionModel(DirectionModelConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||
throw new OcrException("未配置OCR模型");
|
||||
}
|
||||
return directionModelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
|
||||
return createDirectionModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 创建OCR通用检测模型
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private OcrCommonDetModel createCommonDetModel(OcrDetModelConfig config) {
|
||||
Class<?> clazz = commonDetRegistry.get(config.getModelEnum().name().toLowerCase());
|
||||
if(clazz == null){
|
||||
throw new OcrException("Unsupported model");
|
||||
}
|
||||
OcrCommonDetModel model = null;
|
||||
try {
|
||||
model = (OcrCommonDetModel) clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new OcrException(e);
|
||||
}
|
||||
model.loadModel(config);
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建OCR通用识别模型
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private OcrCommonRecModel createCommonRecModel(OcrRecModelConfig config) {
|
||||
Class<?> clazz = commonRecRegistry.get(config.getRecModelEnum().name().toLowerCase());
|
||||
if(clazz == null){
|
||||
throw new OcrException("Unsupported model");
|
||||
}
|
||||
OcrCommonRecModel model = null;
|
||||
try {
|
||||
model = (OcrCommonRecModel) clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new OcrException(e);
|
||||
}
|
||||
model.loadModel(config);
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建OCR方向分类模型
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private OcrDirectionModel createDirectionModel(DirectionModelConfig config) {
|
||||
Class<?> clazz = directionRegistry.get(config.getModelEnum().name().toLowerCase());
|
||||
if(clazz == null){
|
||||
throw new OcrException("Unsupported model");
|
||||
}
|
||||
OcrDirectionModel model = null;
|
||||
try {
|
||||
model = (OcrDirectionModel) clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new OcrException(e);
|
||||
}
|
||||
model.loadModel(config);
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
// 初始化默认算法
|
||||
static {
|
||||
registerCommonDetModel("PADDLEOCR_V5_DET_MODEL", PpOCRV5DetModel.class);
|
||||
registerCommonRecModel("PADDLEOCR_V5_REC_MODEL", PpOCRV5RecModel.class);
|
||||
registerDirectionModel("CH_PPOCR_MOBILE_V2_CLS", PPOCRMobileV2Model.class);
|
||||
log.info("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package cn.smartjavaai.ocr.model.common.detect;
|
||||
|
||||
import ai.djl.modality.cv.Image;
|
||||
import cn.smartjavaai.ocr.config.OcrDetModelConfig;
|
||||
import cn.smartjavaai.ocr.entity.OcrBox;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* OCR 通用检测模型
|
||||
* @author dwj
|
||||
*/
|
||||
public interface OcrCommonDetModel {
|
||||
|
||||
/**
|
||||
* 加载模型
|
||||
* @param config
|
||||
*/
|
||||
void loadModel(OcrDetModelConfig config); // 加载模型
|
||||
|
||||
/**
|
||||
* 文本检测
|
||||
* @param imagePath 图片路径
|
||||
* @return
|
||||
*/
|
||||
default List<OcrBox> detect(String imagePath) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 文本检测
|
||||
* @param image BufferedImage
|
||||
* @return
|
||||
*/
|
||||
default List<OcrBox> detect(BufferedImage image) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 文本检测
|
||||
* @param imageData 图片字节数组
|
||||
* @return
|
||||
*/
|
||||
default List<OcrBox> detect(byte[] imageData) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本检测
|
||||
* @param image DJL Image
|
||||
* @return
|
||||
*/
|
||||
default List<OcrBox> detect(Image image){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测并绘制结果
|
||||
* @param imagePath 图片输入路径(包含文件名称)
|
||||
* @param outputPath 图片输出路径(包含文件名称)
|
||||
*/
|
||||
default void detectAndDraw(String imagePath, String outputPath) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测并绘制结果
|
||||
* @param sourceImage
|
||||
* @return
|
||||
*/
|
||||
default BufferedImage detectAndDraw(BufferedImage sourceImage){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package cn.smartjavaai.ocr.model.common.detect;
|
||||
|
||||
import ai.djl.Device;
|
||||
import ai.djl.MalformedModelException;
|
||||
import ai.djl.engine.Engine;
|
||||
import ai.djl.inference.Predictor;
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.ImageFactory;
|
||||
import ai.djl.modality.cv.output.DetectedObjects;
|
||||
import ai.djl.ndarray.NDList;
|
||||
import ai.djl.ndarray.NDManager;
|
||||
import ai.djl.repository.zoo.Criteria;
|
||||
import ai.djl.repository.zoo.ModelNotFoundException;
|
||||
import ai.djl.repository.zoo.ModelZoo;
|
||||
import ai.djl.repository.zoo.ZooModel;
|
||||
import ai.djl.training.util.ProgressBar;
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.common.pool.PredictorFactory;
|
||||
import cn.smartjavaai.common.utils.FileUtils;
|
||||
import cn.smartjavaai.common.utils.ImageUtils;
|
||||
import cn.smartjavaai.common.utils.OpenCVUtils;
|
||||
import cn.smartjavaai.ocr.config.OcrDetModelConfig;
|
||||
import cn.smartjavaai.ocr.entity.OcrBox;
|
||||
import cn.smartjavaai.ocr.exception.OcrException;
|
||||
import cn.smartjavaai.ocr.model.common.detect.translator.PPOCRV5DetTranslator;
|
||||
import cn.smartjavaai.ocr.utils.OcrUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.pool2.ObjectPool;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||
import org.opencv.core.Mat;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* PPOCRV5 检测模型
|
||||
* @author dwj
|
||||
* @date 2025/4/21
|
||||
*/
|
||||
@Slf4j
|
||||
public class PpOCRV5DetModel implements OcrCommonDetModel {
|
||||
|
||||
|
||||
private ObjectPool<Predictor<Image, NDList>> detPredictorPool;
|
||||
|
||||
private OcrDetModelConfig config;
|
||||
|
||||
@Override
|
||||
public void loadModel(OcrDetModelConfig config){
|
||||
if(StringUtils.isBlank(config.getDetModelPath())){
|
||||
throw new OcrException("modelPath is null");
|
||||
}
|
||||
Device device = null;
|
||||
if(!Objects.isNull(config.getDevice())){
|
||||
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
|
||||
}
|
||||
this.config = config;
|
||||
//初始化 检测Criteria
|
||||
Criteria<Image, NDList> detCriteria =
|
||||
Criteria.builder()
|
||||
.optEngine("OnnxRuntime")
|
||||
.setTypes(Image.class, NDList.class)
|
||||
.optModelPath(Paths.get(config.getDetModelPath()))
|
||||
.optTranslator(new PPOCRV5DetTranslator(new ConcurrentHashMap<String, String>()))
|
||||
.optDevice(device)
|
||||
.optProgress(new ProgressBar())
|
||||
.build();
|
||||
|
||||
try{
|
||||
ZooModel detectionModel = ModelZoo.loadModel(detCriteria);
|
||||
// 创建池子:每个线程独享 Predictor
|
||||
this.detPredictorPool = new GenericObjectPool<>(new PredictorFactory<>(detectionModel));
|
||||
log.info("当前设备: " + detectionModel.getNDManager().getDevice());
|
||||
log.info("当前引擎: " + Engine.getInstance().getEngineName());
|
||||
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
|
||||
throw new OcrException("检测模型加载失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OcrBox> detect(String imagePath){
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new OcrException("图像文件不存在");
|
||||
}
|
||||
Image img = null;
|
||||
try {
|
||||
img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
} catch (IOException e) {
|
||||
throw new OcrException("无效的图片", e);
|
||||
}
|
||||
List<OcrBox> ocrBoxList = detect(img);
|
||||
((Mat)img.getWrappedImage()).release();
|
||||
return ocrBoxList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OcrBox> detect(Image image){
|
||||
Predictor<Image, NDList> predictor = null;
|
||||
try (NDManager manager = NDManager.newBaseManager()) {
|
||||
predictor = detPredictorPool.borrowObject();
|
||||
NDList result = predictor.predict(image);
|
||||
result.attach(manager);
|
||||
return OcrUtils.convertToOcrBox(result, image);
|
||||
} catch (Exception e) {
|
||||
throw new OcrException("OCR检测错误", e);
|
||||
}finally {
|
||||
if (predictor != null) {
|
||||
try {
|
||||
detPredictorPool.returnObject(predictor); //归还
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
try {
|
||||
predictor.close(); // 归还失败才销毁
|
||||
} catch (Exception ex) {
|
||||
log.error("关闭Predictor失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detectAndDraw(String imagePath, String outputPath) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new OcrException("图像文件不存在");
|
||||
}
|
||||
try (NDManager manager = NDManager.newBaseManager()) {
|
||||
Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
List<OcrBox> boxList = detect(img);
|
||||
if(Objects.isNull(boxList) || boxList.isEmpty()){
|
||||
throw new OcrException("未检测到文字");
|
||||
}
|
||||
OcrUtils.drawRect((Mat)img.getWrappedImage(), boxList);
|
||||
Path output = Paths.get(outputPath);
|
||||
log.info("Saving to {}", output.toAbsolutePath().toString());
|
||||
img.save(Files.newOutputStream(output), "png");
|
||||
((Mat) img.getWrappedImage()).release();
|
||||
} catch (IOException e) {
|
||||
throw new OcrException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<OcrBox> detect(BufferedImage image) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new OcrException("图像无效");
|
||||
}
|
||||
Image img = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
|
||||
List<OcrBox> ocrBoxList = detect(img);
|
||||
((Mat)img.getWrappedImage()).release();
|
||||
return ocrBoxList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OcrBox> detect(byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new OcrException("图像无效");
|
||||
}
|
||||
try {
|
||||
BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageData));
|
||||
return detect(image);
|
||||
} catch (IOException e) {
|
||||
throw new OcrException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedImage detectAndDraw(BufferedImage sourceImage) {
|
||||
if(!ImageUtils.isImageValid(sourceImage)){
|
||||
throw new OcrException("图像无效");
|
||||
}
|
||||
Image img = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(sourceImage));
|
||||
List<OcrBox> ocrBoxList = detect(img);
|
||||
if(Objects.isNull(ocrBoxList) || ocrBoxList.isEmpty()){
|
||||
throw new OcrException("未检测到文字");
|
||||
}
|
||||
OcrUtils.drawRect((Mat)img.getWrappedImage(), ocrBoxList);
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
// 调用 save 方法将 Image 写入字节流
|
||||
img.save(outputStream, "png");
|
||||
// 将字节流转换为 BufferedImage
|
||||
byte[] imageBytes = outputStream.toByteArray();
|
||||
((Mat) img.getWrappedImage()).release();
|
||||
return ImageIO.read(new ByteArrayInputStream(imageBytes));
|
||||
} catch (IOException e) {
|
||||
throw new OcrException("导出图片失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.smartjavaai.ocr.ppv4.translator;
|
||||
package cn.smartjavaai.ocr.model.common.detect.translator;
|
||||
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.util.NDImageUtils;
|
||||
@@ -27,7 +27,7 @@ import java.util.Map;
|
||||
* @mail 179209347@qq.com
|
||||
* @website www.aias.top
|
||||
*/
|
||||
public class PaddleOCRV4DetectTranslator implements Translator<Image, NDList> {
|
||||
public class PPOCRV5DetTranslator implements Translator<Image, NDList> {
|
||||
// det_algorithm == "DB"
|
||||
private final float thresh = 0.3f;
|
||||
private final boolean use_dilation = false;
|
||||
@@ -44,7 +44,7 @@ public class PaddleOCRV4DetectTranslator implements Translator<Image, NDList> {
|
||||
private int img_height;
|
||||
private int img_width;
|
||||
|
||||
public PaddleOCRV4DetectTranslator(Map<String, ?> arguments) {
|
||||
public PPOCRV5DetTranslator(Map<String, ?> arguments) {
|
||||
limit_side_len =
|
||||
arguments.containsKey("limit_side_len")
|
||||
? Integer.parseInt(arguments.get("limit_side_len").toString())
|
||||
@@ -0,0 +1,96 @@
|
||||
package cn.smartjavaai.ocr.model.common.direction;
|
||||
|
||||
import ai.djl.inference.Predictor;
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.ndarray.NDManager;
|
||||
import cn.smartjavaai.ocr.config.DirectionModelConfig;
|
||||
import cn.smartjavaai.ocr.entity.DirectionInfo;
|
||||
import cn.smartjavaai.ocr.entity.OcrBox;
|
||||
import cn.smartjavaai.ocr.entity.OcrInfo;
|
||||
import cn.smartjavaai.ocr.entity.OcrItem;
|
||||
import org.opencv.core.Mat;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* OCR 文本方向分类模型
|
||||
* @author dwj
|
||||
*/
|
||||
public interface OcrDirectionModel {
|
||||
|
||||
/**
|
||||
* 加载模型
|
||||
* @param config
|
||||
*/
|
||||
void loadModel(DirectionModelConfig config); // 加载模型
|
||||
|
||||
/**
|
||||
* 文本方向检测
|
||||
* @param imagePath 图片路径
|
||||
* @return
|
||||
*/
|
||||
default List<OcrItem> detect(String imagePath) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 文本方向检测
|
||||
* @param image BufferedImage
|
||||
* @return
|
||||
*/
|
||||
default List<OcrItem> detect(BufferedImage image) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 文本方向检测
|
||||
* @param imageData 图片字节数组
|
||||
* @return
|
||||
*/
|
||||
default List<OcrItem> detect(byte[] imageData) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本方向检测
|
||||
* @param image
|
||||
* @return
|
||||
*/
|
||||
default List<OcrItem> detect(Image image) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 文本方向检测(基于检测结果)
|
||||
* @param boxList
|
||||
* @param srcMat
|
||||
* @param manager
|
||||
* @return
|
||||
*/
|
||||
default List<OcrItem> detect(List<OcrBox> boxList, Mat srcMat, NDManager manager) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测并绘制结果
|
||||
* @param imagePath 图片输入路径(包含文件名称)
|
||||
* @param outputPath 图片输出路径(包含文件名称)
|
||||
*/
|
||||
default void detectAndDraw(String imagePath, String outputPath) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测并绘制结果
|
||||
* @param sourceImage
|
||||
* @return
|
||||
*/
|
||||
default BufferedImage detectAndDraw(BufferedImage sourceImage){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package cn.smartjavaai.ocr.model.common.direction;
|
||||
|
||||
import ai.djl.Device;
|
||||
import ai.djl.MalformedModelException;
|
||||
import ai.djl.engine.Engine;
|
||||
import ai.djl.inference.Predictor;
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.ImageFactory;
|
||||
import ai.djl.modality.cv.output.Point;
|
||||
import ai.djl.ndarray.NDManager;
|
||||
import ai.djl.opencv.OpenCVImageFactory;
|
||||
import ai.djl.repository.zoo.Criteria;
|
||||
import ai.djl.repository.zoo.ModelNotFoundException;
|
||||
import ai.djl.repository.zoo.ModelZoo;
|
||||
import ai.djl.repository.zoo.ZooModel;
|
||||
import ai.djl.training.util.ProgressBar;
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.common.pool.PredictorFactory;
|
||||
import cn.smartjavaai.common.utils.FileUtils;
|
||||
import cn.smartjavaai.common.utils.ImageUtils;
|
||||
import cn.smartjavaai.common.utils.OpenCVUtils;
|
||||
import cn.smartjavaai.ocr.config.DirectionModelConfig;
|
||||
import cn.smartjavaai.ocr.config.OcrDetModelConfig;
|
||||
import cn.smartjavaai.ocr.entity.*;
|
||||
import cn.smartjavaai.ocr.enums.AngleEnum;
|
||||
import cn.smartjavaai.ocr.exception.OcrException;
|
||||
import cn.smartjavaai.ocr.factory.OcrModelFactory;
|
||||
import cn.smartjavaai.ocr.model.common.detect.OcrCommonDetModel;
|
||||
import cn.smartjavaai.ocr.model.common.direction.translator.PpWordRotateTranslator;
|
||||
import cn.smartjavaai.ocr.opencv.OcrNDArrayUtils;
|
||||
import cn.smartjavaai.ocr.opencv.OcrOpenCVUtils;
|
||||
import cn.smartjavaai.ocr.utils.OcrUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.pool2.ObjectPool;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||
import org.opencv.core.Mat;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* PPOCRMobileV2Model 方向分类模型
|
||||
* @author dwj
|
||||
* @date 2025/4/21
|
||||
*/
|
||||
@Slf4j
|
||||
public class PPOCRMobileV2Model implements OcrDirectionModel {
|
||||
|
||||
|
||||
private ObjectPool<Predictor<Image, DirectionInfo>> predictorPool;
|
||||
|
||||
private DirectionModelConfig config;
|
||||
|
||||
private OcrCommonDetModel detModel;
|
||||
|
||||
|
||||
@Override
|
||||
public void loadModel(DirectionModelConfig config){
|
||||
if(StringUtils.isBlank(config.getModelPath())){
|
||||
throw new OcrException("modelPath is null");
|
||||
}
|
||||
|
||||
this.config = config;
|
||||
Device device = null;
|
||||
if(!Objects.isNull(config.getDevice())){
|
||||
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
|
||||
}
|
||||
Criteria<Image, DirectionInfo> criteria =
|
||||
Criteria.builder()
|
||||
.optEngine("OnnxRuntime")
|
||||
.setTypes(Image.class, DirectionInfo.class)
|
||||
.optModelPath(Paths.get(config.getModelPath()))
|
||||
.optDevice(device)
|
||||
.optTranslator(new PpWordRotateTranslator())
|
||||
.optProgress(new ProgressBar())
|
||||
.build();
|
||||
try{
|
||||
ZooModel model = ModelZoo.loadModel(criteria);
|
||||
// 创建池子:每个线程独享 Predictor
|
||||
this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model));
|
||||
log.info("当前设备: " + model.getNDManager().getDevice());
|
||||
log.info("当前引擎: " + Engine.getInstance().getEngineName());
|
||||
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
|
||||
throw new OcrException("模型加载失败", e);
|
||||
}
|
||||
|
||||
//获取检测模型
|
||||
if(StringUtils.isNotBlank(config.getDetModelPath()) && Objects.nonNull(config.getDetModelEnum())){
|
||||
OcrDetModelConfig detModelConfig = new OcrDetModelConfig();
|
||||
detModelConfig.setModelEnum(config.getDetModelEnum());
|
||||
detModelConfig.setDetModelPath(config.getDetModelPath());
|
||||
detModel = OcrModelFactory.getInstance().getDetModel(detModelConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OcrItem> detect(String imagePath){
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new OcrException("图像文件不存在");
|
||||
}
|
||||
Image img = null;
|
||||
try {
|
||||
img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
} catch (IOException e) {
|
||||
throw new OcrException("无效的图片", e);
|
||||
}
|
||||
List<OcrItem> ocrItemList = detect(img);
|
||||
((Mat)img.getWrappedImage()).release();
|
||||
return ocrItemList;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<OcrItem> detect(Image image){
|
||||
//检测文本
|
||||
List<OcrBox> boxeList = detModel.detect(image);
|
||||
if(Objects.isNull(boxeList) || boxeList.isEmpty()){
|
||||
throw new OcrException("未检测到文本");
|
||||
}
|
||||
Predictor<Image, DirectionInfo> predictor = null;
|
||||
List<OcrItem> ocrItemList = new ArrayList<>();
|
||||
try (NDManager manager = NDManager.newBaseManager()) {
|
||||
Mat srcMat = (Mat) image.getWrappedImage();
|
||||
predictor = predictorPool.borrowObject();
|
||||
for (OcrBox box : boxeList){
|
||||
OcrItem ocrItem = detect(box, srcMat, predictor, manager);
|
||||
ocrItemList.add(ocrItem);
|
||||
}
|
||||
return ocrItemList;
|
||||
} catch (Exception e) {
|
||||
throw new OcrException("OCR检测错误", e);
|
||||
}finally {
|
||||
if (predictor != null) {
|
||||
try {
|
||||
predictorPool.returnObject(predictor); //归还
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
try {
|
||||
predictor.close(); // 归还失败才销毁
|
||||
} catch (Exception ex) {
|
||||
log.error("关闭Predictor失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 基于文本框检测方向
|
||||
* @param box
|
||||
* @param srcMat
|
||||
* @param predictor
|
||||
* @param manager
|
||||
* @return
|
||||
*/
|
||||
private OcrItem detect(OcrBox box, Mat srcMat, Predictor<Image, DirectionInfo> predictor, NDManager manager){
|
||||
if(Objects.isNull(box)){
|
||||
throw new OcrException("box参数为空");
|
||||
}
|
||||
try {
|
||||
//透视变换及裁剪
|
||||
Image subImg = OcrUtils.transformAndCrop(srcMat, box);
|
||||
DirectionInfo directionInfo = null;
|
||||
String angle;
|
||||
//高宽比 > 1.5 纵向
|
||||
if (subImg.getHeight() * 1.0 / subImg.getWidth() > 1.5) {
|
||||
//旋转图片90度
|
||||
subImg = OcrUtils.rotateImg(manager, subImg);
|
||||
//ImageUtils.saveImage(subImg, i + "rotate.png", "build/output");
|
||||
//检测方向
|
||||
directionInfo = predictor.predict(subImg);
|
||||
if (directionInfo.getName().equalsIgnoreCase("Rotate")) {
|
||||
angle = "90";
|
||||
} else {
|
||||
angle = "270";
|
||||
}
|
||||
}else{ //横向
|
||||
directionInfo = predictor.predict(subImg);
|
||||
if (directionInfo.getName().equalsIgnoreCase("No Rotate")) {
|
||||
angle = "0";
|
||||
} else {
|
||||
angle = "180";
|
||||
}
|
||||
}
|
||||
((Mat)subImg.getWrappedImage()).release();
|
||||
return new OcrItem(box, AngleEnum.fromValue(angle), directionInfo.getProb().floatValue());
|
||||
} catch (Exception e) {
|
||||
throw new OcrException("OCR检测错误", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OcrItem> detect(List<OcrBox> boxList,Mat srcMat,NDManager manager){
|
||||
if(Objects.isNull(boxList) || boxList.isEmpty()){
|
||||
throw new OcrException("boxList为空");
|
||||
}
|
||||
Predictor<Image, DirectionInfo> predictor = null;
|
||||
List<OcrItem> ocrItemList = new ArrayList<>();
|
||||
try {
|
||||
predictor = predictorPool.borrowObject();
|
||||
for (OcrBox box : boxList){
|
||||
OcrItem ocrItem = detect(box, srcMat, predictor, manager);
|
||||
ocrItemList.add(ocrItem);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new OcrException("OCR检测错误", e);
|
||||
}
|
||||
return ocrItemList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detectAndDraw(String imagePath, String outputPath) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new OcrException("图像文件不存在");
|
||||
}
|
||||
try (NDManager manager = NDManager.newBaseManager()) {
|
||||
Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
List<OcrItem> itemList = detect(img);
|
||||
if(Objects.isNull(itemList) || itemList.isEmpty()){
|
||||
throw new OcrException("未检测到文字");
|
||||
}
|
||||
OcrUtils.drawRectWithText((Mat) img.getWrappedImage(), itemList);
|
||||
Path output = Paths.get(outputPath);
|
||||
log.info("Saving to {}", output.toAbsolutePath().toString());
|
||||
img.save(Files.newOutputStream(output), "png");
|
||||
((Mat) img.getWrappedImage()).release();
|
||||
} catch (IOException e) {
|
||||
throw new OcrException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OcrItem> detect(BufferedImage image) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new OcrException("图像无效");
|
||||
}
|
||||
Image img = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
|
||||
List<OcrItem> ocrItemList = detect(img);
|
||||
((Mat)img.getWrappedImage()).release();
|
||||
return ocrItemList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OcrItem> detect(byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new OcrException("图像无效");
|
||||
}
|
||||
try {
|
||||
BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageData));
|
||||
return detect(image);
|
||||
} catch (IOException e) {
|
||||
throw new OcrException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedImage detectAndDraw(BufferedImage sourceImage) {
|
||||
if(!ImageUtils.isImageValid(sourceImage)){
|
||||
throw new OcrException("图像无效");
|
||||
}
|
||||
Image img = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(sourceImage));
|
||||
List<OcrItem> ocrItemList = detect(img);
|
||||
if(Objects.isNull(ocrItemList) || ocrItemList.isEmpty()){
|
||||
throw new OcrException("未检测到文字");
|
||||
}
|
||||
OcrUtils.drawRectWithText((Mat) img.getWrappedImage(), ocrItemList);
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
// 调用 save 方法将 Image 写入字节流
|
||||
img.save(outputStream, "png");
|
||||
// 将字节流转换为 BufferedImage
|
||||
byte[] imageBytes = outputStream.toByteArray();
|
||||
((Mat) img.getWrappedImage()).release();
|
||||
return ImageIO.read(new ByteArrayInputStream(imageBytes));
|
||||
} catch (IOException e) {
|
||||
throw new OcrException("导出图片失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package cn.smartjavaai.ocr.model.common.direction.translator;
|
||||
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.util.NDImageUtils;
|
||||
import ai.djl.ndarray.NDArray;
|
||||
import ai.djl.ndarray.NDList;
|
||||
import ai.djl.ndarray.index.NDIndex;
|
||||
import ai.djl.ndarray.types.Shape;
|
||||
import ai.djl.translate.Batchifier;
|
||||
import ai.djl.translate.Translator;
|
||||
import ai.djl.translate.TranslatorContext;
|
||||
import cn.smartjavaai.ocr.entity.DirectionInfo;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 方向检测
|
||||
*
|
||||
* @author Calvin
|
||||
* @mail 179209347@qq.com
|
||||
* @website www.aias.top
|
||||
*/
|
||||
public class PpWordRotateTranslator implements Translator<Image, DirectionInfo> {
|
||||
List<String> classes = Arrays.asList("No Rotate", "Rotate");
|
||||
|
||||
public PpWordRotateTranslator() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DirectionInfo processOutput(TranslatorContext ctx, NDList list) {
|
||||
NDArray prob = list.singletonOrThrow();
|
||||
float[] res = prob.toFloatArray();
|
||||
int maxIndex = 0;
|
||||
if (res[1] > res[0]) {
|
||||
maxIndex = 1;
|
||||
}
|
||||
|
||||
return new DirectionInfo(classes.get(maxIndex), Double.valueOf(res[maxIndex]));
|
||||
}
|
||||
|
||||
// public NDList processInput2(TranslatorContext ctx, Image input){
|
||||
// NDArray img = input.toNDArray(ctx.getNDManager());
|
||||
// img = NDImageUtils.resize(img, 192, 48);
|
||||
// img = NDImageUtils.toTensor(img).sub(0.5F).div(0.5F);
|
||||
// img = img.expandDims(0);
|
||||
// return new NDList(new NDArray[]{img});
|
||||
// }
|
||||
|
||||
@Override
|
||||
public NDList processInput(TranslatorContext ctx, Image input) {
|
||||
NDArray img = input.toNDArray(ctx.getNDManager());
|
||||
int imgC = 3;
|
||||
int imgH = 48;
|
||||
int imgW = 192;
|
||||
|
||||
NDArray array = ctx.getNDManager().zeros(new Shape(imgC, imgH, imgW));
|
||||
|
||||
int h = input.getHeight();
|
||||
int w = input.getWidth();
|
||||
int resized_w = 0;
|
||||
|
||||
float ratio = (float) w / (float) h;
|
||||
if (Math.ceil(imgH * ratio) > imgW) {
|
||||
resized_w = imgW;
|
||||
} else {
|
||||
resized_w = (int) (Math.ceil(imgH * ratio));
|
||||
}
|
||||
|
||||
img = NDImageUtils.resize(img, resized_w, imgH);
|
||||
|
||||
img = NDImageUtils.toTensor(img).sub(0.5F).div(0.5F);
|
||||
// img = img.transpose(2, 0, 1);
|
||||
|
||||
array.set(new NDIndex(":,:,0:" + resized_w), img);
|
||||
|
||||
array = array.expandDims(0);
|
||||
|
||||
return new NDList(new NDArray[]{array});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Batchifier getBatchifier() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package cn.smartjavaai.ocr.model.common.recognize;
|
||||
|
||||
import ai.djl.modality.cv.Image;
|
||||
import cn.smartjavaai.ocr.config.OcrDetModelConfig;
|
||||
import cn.smartjavaai.ocr.config.OcrRecModelConfig;
|
||||
import cn.smartjavaai.ocr.entity.OcrBox;
|
||||
import cn.smartjavaai.ocr.entity.OcrInfo;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* OCR 通用识别模型
|
||||
* @author dwj
|
||||
*/
|
||||
public interface OcrCommonRecModel {
|
||||
|
||||
/**
|
||||
* 加载模型
|
||||
* @param config
|
||||
*/
|
||||
void loadModel(OcrRecModelConfig config); // 加载模型
|
||||
|
||||
/**
|
||||
* 文本识别
|
||||
* @param imagePath 图片路径
|
||||
* @return
|
||||
*/
|
||||
default OcrInfo recognize(String imagePath) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 文本检测
|
||||
* @param image BufferedImage
|
||||
* @return
|
||||
*/
|
||||
default OcrInfo recognize(BufferedImage image) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 文本检测
|
||||
* @param imageData 图片字节数组
|
||||
* @return
|
||||
*/
|
||||
default OcrInfo recognize(byte[] imageData) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 识别并绘制结果
|
||||
* @param imagePath
|
||||
* @param outputPath
|
||||
*/
|
||||
default void recognizeAndDraw(String imagePath, String outputPath, int fontSize) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别并绘制结果
|
||||
* @param sourceImage
|
||||
* @return
|
||||
*/
|
||||
default BufferedImage recognizeAndDraw(BufferedImage sourceImage, int fontSize){
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package cn.smartjavaai.ocr.model.common.recognize;
|
||||
|
||||
import ai.djl.Device;
|
||||
import ai.djl.MalformedModelException;
|
||||
import ai.djl.engine.Engine;
|
||||
import ai.djl.inference.Predictor;
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.ImageFactory;
|
||||
import ai.djl.modality.cv.output.Point;
|
||||
import ai.djl.ndarray.NDArray;
|
||||
import ai.djl.ndarray.NDList;
|
||||
import ai.djl.ndarray.NDManager;
|
||||
import ai.djl.opencv.OpenCVImageFactory;
|
||||
import ai.djl.repository.zoo.Criteria;
|
||||
import ai.djl.repository.zoo.ModelNotFoundException;
|
||||
import ai.djl.repository.zoo.ModelZoo;
|
||||
import ai.djl.repository.zoo.ZooModel;
|
||||
import ai.djl.training.util.ProgressBar;
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.common.pool.PredictorFactory;
|
||||
import cn.smartjavaai.common.utils.FileUtils;
|
||||
import cn.smartjavaai.common.utils.ImageUtils;
|
||||
import cn.smartjavaai.common.utils.OpenCVUtils;
|
||||
import cn.smartjavaai.ocr.config.DirectionModelConfig;
|
||||
import cn.smartjavaai.ocr.config.OcrDetModelConfig;
|
||||
import cn.smartjavaai.ocr.config.OcrRecModelConfig;
|
||||
import cn.smartjavaai.ocr.entity.*;
|
||||
import cn.smartjavaai.ocr.exception.OcrException;
|
||||
import cn.smartjavaai.ocr.factory.OcrModelFactory;
|
||||
import cn.smartjavaai.ocr.model.common.detect.OcrCommonDetModel;
|
||||
import cn.smartjavaai.ocr.model.common.detect.translator.PPOCRV5DetTranslator;
|
||||
import cn.smartjavaai.ocr.model.common.direction.OcrDirectionModel;
|
||||
import cn.smartjavaai.ocr.model.common.recognize.translator.PPOCRV5RecTranslator;
|
||||
import cn.smartjavaai.ocr.opencv.OcrNDArrayUtils;
|
||||
import cn.smartjavaai.ocr.opencv.OcrOpenCVUtils;
|
||||
import cn.smartjavaai.ocr.utils.OcrUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.pool2.ObjectPool;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||
import org.opencv.core.Mat;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* PPOCRV5 识别模型
|
||||
* @author dwj
|
||||
* @date 2025/4/21
|
||||
*/
|
||||
@Slf4j
|
||||
public class PpOCRV5RecModel implements OcrCommonRecModel {
|
||||
|
||||
private ObjectPool<Predictor<Image, String>> recPredictorPool;
|
||||
|
||||
private OcrRecModelConfig config;
|
||||
|
||||
private OcrCommonDetModel detModel;
|
||||
|
||||
private OcrDirectionModel directionModel;
|
||||
|
||||
@Override
|
||||
public void loadModel(OcrRecModelConfig config){
|
||||
if(StringUtils.isBlank(config.getRecModelPath())){
|
||||
throw new OcrException("recModelPath is null");
|
||||
}
|
||||
Device device = null;
|
||||
if(!Objects.isNull(config.getDevice())){
|
||||
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
|
||||
}
|
||||
this.config = config;
|
||||
//初始化 识别Criteria
|
||||
Criteria<Image, String> recCriteria =
|
||||
Criteria.builder()
|
||||
.optEngine("OnnxRuntime")
|
||||
.setTypes(Image.class, String.class)
|
||||
.optModelPath(Paths.get(config.getRecModelPath()))
|
||||
.optTranslator(new PPOCRV5RecTranslator(new ConcurrentHashMap<String, String>()))
|
||||
.optProgress(new ProgressBar())
|
||||
.optDevice(device)
|
||||
.build();
|
||||
try{
|
||||
ZooModel recognitionModel = ModelZoo.loadModel(recCriteria);
|
||||
this.recPredictorPool = new GenericObjectPool<>(new PredictorFactory<>(recognitionModel));
|
||||
log.info("当前设备: " + recognitionModel.getNDManager().getDevice());
|
||||
log.info("当前引擎: " + Engine.getInstance().getEngineName());
|
||||
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
|
||||
throw new OcrException("识别模型加载失败", e);
|
||||
}
|
||||
|
||||
|
||||
//获取检测模型
|
||||
if(StringUtils.isNotBlank(config.getDetModelPath()) && Objects.nonNull(config.getDetModelEnum())){
|
||||
OcrDetModelConfig detModelConfig = new OcrDetModelConfig();
|
||||
detModelConfig.setModelEnum(config.getDetModelEnum());
|
||||
detModelConfig.setDetModelPath(config.getDetModelPath());
|
||||
detModel = OcrModelFactory.getInstance().getDetModel(detModelConfig);
|
||||
}
|
||||
|
||||
//获取方向检测模型
|
||||
if(StringUtils.isNotBlank(config.getDirectionModelPath()) && Objects.nonNull(config.getDirectionModelEnum())){
|
||||
DirectionModelConfig directionModelConfig = new DirectionModelConfig();
|
||||
directionModelConfig.setModelEnum(config.getDirectionModelEnum());
|
||||
directionModelConfig.setModelPath(config.getDirectionModelPath());
|
||||
directionModel = OcrModelFactory.getInstance().getDirectionModel(directionModelConfig);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public OcrInfo recognize(String imagePath) {
|
||||
if(StringUtils.isBlank(config.getRecModelPath())){
|
||||
throw new OcrException("recModelPath为空,无法识别");
|
||||
}
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new OcrException("图像文件不存在");
|
||||
}
|
||||
Image img = null;
|
||||
try {
|
||||
img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
} catch (IOException e) {
|
||||
throw new OcrException("无效的图片", e);
|
||||
}
|
||||
OcrInfo ocrInfo = recognize(img);
|
||||
((Mat)img.getWrappedImage()).release();
|
||||
return ocrInfo;
|
||||
}
|
||||
|
||||
private OcrInfo recognize(Image image) {
|
||||
//检测文本
|
||||
List<OcrBox> boxeList = detModel.detect(image);
|
||||
if(Objects.isNull(boxeList) || boxeList.isEmpty()){
|
||||
throw new OcrException("未检测到文本");
|
||||
}
|
||||
Predictor<Image, String> predictor = null;
|
||||
List<RotatedBox> rotatedBoxes = new ArrayList<>();
|
||||
List<OcrItem> ocrItemList = new ArrayList<>();
|
||||
try (NDManager manager = NDManager.newBaseManager()) {
|
||||
Mat srcMat = (Mat) image.getWrappedImage();
|
||||
predictor = recPredictorPool.borrowObject();
|
||||
//检测方向
|
||||
if(directionModel != null){
|
||||
ocrItemList = directionModel.detect(boxeList, srcMat, manager);
|
||||
if(Objects.isNull(ocrItemList) || ocrItemList.isEmpty()){
|
||||
throw new OcrException("方向检测失败");
|
||||
}
|
||||
for (OcrItem ocrItem : ocrItemList){
|
||||
//放射变换+裁剪
|
||||
Image subImage = OcrUtils.transformAndCrop(srcMat, ocrItem.getOcrBox());
|
||||
//纠正文本框
|
||||
subImage = OcrUtils.rotateImg(subImage, ocrItem.getAngle());
|
||||
//识别
|
||||
String name = predictor.predict(subImage);
|
||||
ocrItem.setText(name);
|
||||
NDArray ndArray = manager.create(ocrItem.getOcrBox().toFloatArray());
|
||||
rotatedBoxes.add(new RotatedBox(ndArray, ocrItem.getText()));
|
||||
((Mat)subImage.getWrappedImage()).release();
|
||||
}
|
||||
}else{
|
||||
for (OcrBox box : boxeList){
|
||||
RotatedBox rotatedBox = recognize(box, srcMat, predictor, manager);
|
||||
rotatedBoxes.add(rotatedBox);
|
||||
}
|
||||
}
|
||||
//后处理
|
||||
return postProcessOcrResult(rotatedBoxes);
|
||||
} catch (Exception e) {
|
||||
throw new OcrException("OCR检测错误", e);
|
||||
}finally {
|
||||
if (predictor != null) {
|
||||
try {
|
||||
recPredictorPool.returnObject(predictor); //归还
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
try {
|
||||
predictor.close(); // 归还失败才销毁
|
||||
} catch (Exception ex) {
|
||||
log.error("关闭Predictor失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private RotatedBox recognize(OcrBox box,Mat srcMat,Predictor<Image, String> recPredictor,NDManager manager){
|
||||
try {
|
||||
//透视变换 + 裁剪
|
||||
Image subImg = OcrUtils.transformAndCrop(srcMat, box);
|
||||
//ImageUtils.saveImage(subImg, i + "crop.png", "build/output");
|
||||
//高宽比 > 1.5
|
||||
if (subImg.getHeight() * 1.0 / subImg.getWidth() > 1.5) {
|
||||
//旋转图片90度
|
||||
subImg = OcrUtils.rotateImg(manager, subImg);
|
||||
//ImageUtils.saveImage(subImg, i + "rotate.png", "build/output");
|
||||
}
|
||||
String name = recPredictor.predict(subImg);
|
||||
((Mat)subImg.getWrappedImage()).release();
|
||||
NDArray pointsArray = manager.create(box.toFloatArray());
|
||||
return new RotatedBox(pointsArray, name);
|
||||
} catch (Exception e) {
|
||||
throw new OcrException("OCR检测错误", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 后处理:排序,分行
|
||||
* @param rotatedBoxes
|
||||
*/
|
||||
private OcrInfo postProcessOcrResult(List<RotatedBox> rotatedBoxes){
|
||||
//Y坐标升序排序
|
||||
List<RotatedBox> initList = new ArrayList<>();
|
||||
for (RotatedBox result : rotatedBoxes) {
|
||||
initList.add(result);
|
||||
}
|
||||
Collections.sort(initList);
|
||||
//多行文本框的集合
|
||||
List<ArrayList<RotatedBoxCompX>> lines = new ArrayList<>();
|
||||
List<RotatedBoxCompX> line = new ArrayList<>();
|
||||
RotatedBoxCompX firstBox = new RotatedBoxCompX(initList.get(0).getBox(), initList.get(0).getText());
|
||||
line.add(firstBox);
|
||||
lines.add((ArrayList) line);
|
||||
//分行判断
|
||||
for (int i = 1; i < initList.size(); i++) {
|
||||
RotatedBoxCompX tmpBox = new RotatedBoxCompX(initList.get(i).getBox(), initList.get(i).getText());
|
||||
float y1 = firstBox.getBox().toFloatArray()[1];
|
||||
float y2 = tmpBox.getBox().toFloatArray()[1];
|
||||
float dis = Math.abs(y2 - y1);
|
||||
if (dis < 20) { // 认为是同 1 行 - Considered to be in the same line
|
||||
line.add(tmpBox);
|
||||
} else { // 换行 - Line break
|
||||
firstBox = tmpBox;
|
||||
Collections.sort(line);
|
||||
line = new ArrayList<>();
|
||||
line.add(firstBox);
|
||||
lines.add((ArrayList) line);
|
||||
}
|
||||
}
|
||||
return OcrUtils.convertToOcrInfo(lines);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void recognizeAndDraw(String imagePath, String outputPath, int fontSize) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new OcrException("图像文件不存在");
|
||||
}
|
||||
try {
|
||||
Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
OcrInfo ocrInfo = recognize(img);
|
||||
if(Objects.isNull(ocrInfo) || Objects.isNull(ocrInfo.getLineList()) || ocrInfo.getLineList().isEmpty()){
|
||||
throw new OcrException("未检测到文字");
|
||||
}
|
||||
Mat wrappedImage = (Mat) img.getWrappedImage();
|
||||
BufferedImage bufferedImage = OcrOpenCVUtils.mat2Image(wrappedImage);
|
||||
OcrUtils.drawRectWithText(bufferedImage, ocrInfo, fontSize);
|
||||
ImageUtils.saveImage(bufferedImage, outputPath);
|
||||
wrappedImage.release();
|
||||
} catch (IOException e) {
|
||||
throw new OcrException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OcrInfo recognize(BufferedImage image) {
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new OcrException("图像无效");
|
||||
}
|
||||
Image img = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(image));
|
||||
OcrInfo ocrInfo = recognize(img);
|
||||
((Mat)img.getWrappedImage()).release();
|
||||
return ocrInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OcrInfo recognize(byte[] imageData) {
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new OcrException("图像无效");
|
||||
}
|
||||
try {
|
||||
BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageData));
|
||||
return recognize(image);
|
||||
} catch (IOException e) {
|
||||
throw new OcrException("错误的图像", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedImage recognizeAndDraw(BufferedImage sourceImage, int fontSize) {
|
||||
if(!ImageUtils.isImageValid(sourceImage)){
|
||||
throw new OcrException("图像无效");
|
||||
}
|
||||
Image img = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(sourceImage));
|
||||
OcrInfo ocrInfo = recognize(img);
|
||||
if(Objects.isNull(ocrInfo) || Objects.isNull(ocrInfo.getLineList()) || ocrInfo.getLineList().isEmpty()){
|
||||
throw new OcrException("未检测到文字");
|
||||
}
|
||||
try {
|
||||
OcrUtils.drawRectWithText(sourceImage, ocrInfo, fontSize);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
// 调用 save 方法将 Image 写入字节流
|
||||
img.save(outputStream, "png");
|
||||
// 将字节流转换为 BufferedImage
|
||||
byte[] imageBytes = outputStream.toByteArray();
|
||||
((Mat) img.getWrappedImage()).release();
|
||||
return ImageIO.read(new ByteArrayInputStream(imageBytes));
|
||||
} catch (IOException e) {
|
||||
throw new OcrException("导出图片失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cn.smartjavaai.ocr.ppv4.translator;
|
||||
package cn.smartjavaai.ocr.model.common.recognize.translator;
|
||||
|
||||
import ai.djl.Model;
|
||||
import ai.djl.modality.cv.Image;
|
||||
@@ -23,11 +23,11 @@ import java.util.Map;
|
||||
* 文字识别前后处理
|
||||
*
|
||||
*/
|
||||
public class PaddleOCRV4WordRecTranslator implements Translator<Image, String> {
|
||||
public class PPOCRV5RecTranslator implements Translator<Image, String> {
|
||||
private List<String> table;
|
||||
private final boolean use_space_char;
|
||||
|
||||
public PaddleOCRV4WordRecTranslator(Map<String, ?> arguments) {
|
||||
public PPOCRV5RecTranslator(Map<String, ?> arguments) {
|
||||
use_space_char =
|
||||
arguments.containsKey("use_space_char")
|
||||
? Boolean.parseBoolean(arguments.get("use_space_char").toString())
|
||||
@@ -1,126 +0,0 @@
|
||||
package cn.smartjavaai.ocr.ppv4.model;
|
||||
|
||||
import ai.djl.MalformedModelException;
|
||||
import ai.djl.inference.Predictor;
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.ImageFactory;
|
||||
import ai.djl.ndarray.NDList;
|
||||
import ai.djl.repository.zoo.Criteria;
|
||||
import ai.djl.repository.zoo.ModelNotFoundException;
|
||||
import ai.djl.repository.zoo.ModelZoo;
|
||||
import ai.djl.repository.zoo.ZooModel;
|
||||
import ai.djl.training.util.ProgressBar;
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.pool.PredictorFactory;
|
||||
import cn.smartjavaai.common.utils.FileUtils;
|
||||
import cn.smartjavaai.ocr.detection.OcrDetModelConfig;
|
||||
import cn.smartjavaai.ocr.exception.OcrException;
|
||||
import cn.smartjavaai.ocr.detection.OcrDetModel;
|
||||
import cn.smartjavaai.ocr.ppv4.translator.PaddleOCRV4DetectTranslator;
|
||||
import cn.smartjavaai.ocr.utils.ImageUtils;
|
||||
import cn.smartjavaai.ocr.utils.OcrUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.pool2.ObjectPool;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||
import org.opencv.core.Mat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
* @date 2025/4/21
|
||||
*/
|
||||
@Slf4j
|
||||
public class PaddleOCRV4DetModel implements OcrDetModel {
|
||||
|
||||
private ZooModel detectionModel;
|
||||
|
||||
private ObjectPool<Predictor<Image, NDList>> predictorPool;
|
||||
|
||||
@Override
|
||||
public void loadModel(OcrDetModelConfig config){
|
||||
if(StringUtils.isBlank(config.getModelPath())){
|
||||
throw new OcrException("modelPath is null");
|
||||
}
|
||||
Criteria<Image, NDList> criteria =
|
||||
Criteria.builder()
|
||||
.optEngine("OnnxRuntime")
|
||||
.setTypes(Image.class, NDList.class)
|
||||
.optModelPath(Paths.get(config.getModelPath()))
|
||||
.optTranslator(new PaddleOCRV4DetectTranslator(new ConcurrentHashMap<String, String>()))
|
||||
.optProgress(new ProgressBar())
|
||||
.build();
|
||||
try{
|
||||
detectionModel = ModelZoo.loadModel(criteria);
|
||||
// 创建池子:每个线程独享 Predictor
|
||||
this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(detectionModel));
|
||||
log.info("当前设备: " + detectionModel.getNDManager().getDevice());
|
||||
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
|
||||
throw new OcrException("模型加载失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DetectionResponse detect(String imagePath){
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new OcrException("图像文件不存在");
|
||||
}
|
||||
Image img = null;
|
||||
try {
|
||||
img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
} catch (IOException e) {
|
||||
throw new OcrException("无效的图片", e);
|
||||
}
|
||||
return detect(img);
|
||||
}
|
||||
|
||||
private DetectionResponse detect(Image image){
|
||||
Predictor<Image, NDList> predictor = null;
|
||||
try {
|
||||
predictor = predictorPool.borrowObject();
|
||||
NDList result = predictor.predict(image);
|
||||
return OcrUtils.convertToDetectionResponse(result, image);
|
||||
} catch (Exception e) {
|
||||
throw new OcrException("OCR检测错误", e);
|
||||
}finally {
|
||||
if (predictor != null) {
|
||||
try {
|
||||
predictorPool.returnObject(predictor); //归还
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
try {
|
||||
predictor.close(); // 归还失败才销毁
|
||||
} catch (Exception ex) {
|
||||
log.error("关闭Predictor失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detectAndDraw(String imagePath, String outputPath) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new OcrException("图像文件不存在");
|
||||
}
|
||||
try {
|
||||
Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
DetectionResponse detectionResponse = detect(img);
|
||||
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getDetectionInfoList()) || detectionResponse.getDetectionInfoList().isEmpty()){
|
||||
throw new OcrException("未识别到文字");
|
||||
}
|
||||
ImageUtils.drawRect((Mat)img.getWrappedImage(), detectionResponse);
|
||||
Path output = Paths.get(outputPath);
|
||||
log.info("Saving to {}", output.toAbsolutePath().toString());
|
||||
img.save(Files.newOutputStream(output), "png");
|
||||
} catch (IOException e) {
|
||||
throw new OcrException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
package cn.smartjavaai.ocr.ppv4.model;
|
||||
|
||||
import ai.djl.MalformedModelException;
|
||||
import ai.djl.inference.Predictor;
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.ImageFactory;
|
||||
import ai.djl.ndarray.NDList;
|
||||
import ai.djl.repository.zoo.Criteria;
|
||||
import ai.djl.repository.zoo.ModelNotFoundException;
|
||||
import ai.djl.repository.zoo.ModelZoo;
|
||||
import ai.djl.repository.zoo.ZooModel;
|
||||
import ai.djl.training.util.ProgressBar;
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.pool.PredictorFactory;
|
||||
import cn.smartjavaai.common.utils.FileUtils;
|
||||
import cn.smartjavaai.ocr.detection.OcrDetModel;
|
||||
import cn.smartjavaai.ocr.detection.OcrDetModelConfig;
|
||||
import cn.smartjavaai.ocr.exception.OcrException;
|
||||
import cn.smartjavaai.ocr.ppv4.translator.PaddleOCRV4DetectTranslator;
|
||||
import cn.smartjavaai.ocr.recognition.OcrRecModel;
|
||||
import cn.smartjavaai.ocr.recognition.OcrRecModelConfig;
|
||||
import cn.smartjavaai.ocr.utils.ImageUtils;
|
||||
import cn.smartjavaai.ocr.utils.OcrUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.pool2.ObjectPool;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||
import org.opencv.core.Mat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* PaddleOCRV4 识别模型实现
|
||||
* @author dwj
|
||||
* @date 2025/4/21
|
||||
*/
|
||||
@Slf4j
|
||||
public class PaddleOCRV4RecModel implements OcrRecModel {
|
||||
|
||||
private ZooModel detectionModel;
|
||||
|
||||
private ObjectPool<Predictor<Image, NDList>> predictorPool;
|
||||
|
||||
@Override
|
||||
public void loadModel(OcrRecModelConfig config){
|
||||
if(StringUtils.isBlank(config.getModelPath())){
|
||||
throw new OcrException("modelPath is null");
|
||||
}
|
||||
Criteria<Image, NDList> criteria =
|
||||
Criteria.builder()
|
||||
.optEngine("OnnxRuntime")
|
||||
.setTypes(Image.class, NDList.class)
|
||||
.optModelPath(Paths.get(config.getModelPath()))
|
||||
.optTranslator(new PaddleOCRV4DetectTranslator(new ConcurrentHashMap<String, String>()))
|
||||
.optProgress(new ProgressBar())
|
||||
.build();
|
||||
try{
|
||||
detectionModel = ModelZoo.loadModel(criteria);
|
||||
// 创建池子:每个线程独享 Predictor
|
||||
this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(detectionModel));
|
||||
log.info("当前设备: " + detectionModel.getNDManager().getDevice());
|
||||
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
|
||||
throw new OcrException("模型加载失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DetectionResponse detect(String imagePath){
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new OcrException("图像文件不存在");
|
||||
}
|
||||
Image img = null;
|
||||
try {
|
||||
img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
} catch (IOException e) {
|
||||
throw new OcrException("无效的图片", e);
|
||||
}
|
||||
return detect(img);
|
||||
}
|
||||
|
||||
private DetectionResponse detect(Image image){
|
||||
Predictor<Image, NDList> predictor = null;
|
||||
try {
|
||||
predictor = predictorPool.borrowObject();
|
||||
NDList result = predictor.predict(image);
|
||||
return OcrUtils.convertToDetectionResponse(result, image);
|
||||
} catch (Exception e) {
|
||||
throw new OcrException("OCR检测错误", e);
|
||||
}finally {
|
||||
if (predictor != null) {
|
||||
try {
|
||||
predictorPool.returnObject(predictor); //归还
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
try {
|
||||
predictor.close(); // 归还失败才销毁
|
||||
} catch (Exception ex) {
|
||||
log.error("关闭Predictor失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detectAndDraw(String imagePath, String outputPath) {
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new OcrException("图像文件不存在");
|
||||
}
|
||||
try {
|
||||
Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
DetectionResponse detectionResponse = detect(img);
|
||||
if(Objects.isNull(detectionResponse) || Objects.isNull(detectionResponse.getDetectionInfoList()) || detectionResponse.getDetectionInfoList().isEmpty()){
|
||||
throw new OcrException("未识别到文字");
|
||||
}
|
||||
ImageUtils.drawRect((Mat)img.getWrappedImage(), detectionResponse);
|
||||
Path output = Paths.get(outputPath);
|
||||
log.info("Saving to {}", output.toAbsolutePath().toString());
|
||||
img.save(Files.newOutputStream(output), "png");
|
||||
} catch (IOException e) {
|
||||
throw new OcrException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package cn.smartjavaai.ocr.recognition;
|
||||
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
|
||||
/**
|
||||
* OCR模型
|
||||
* @author dwj
|
||||
*/
|
||||
public interface OcrRecModel {
|
||||
|
||||
/**
|
||||
* 加载模型
|
||||
* @param config
|
||||
*/
|
||||
void loadModel(OcrRecModelConfig config); // 加载模型
|
||||
|
||||
/**
|
||||
* 人脸检测
|
||||
* @param imagePath 图片路径
|
||||
* @return
|
||||
*/
|
||||
default DetectionResponse detect(String imagePath) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测并绘制结果
|
||||
* @param imagePath 图片输入路径(包含文件名称)
|
||||
* @param outputPath 图片输出路径(包含文件名称)
|
||||
*/
|
||||
default void detectAndDraw(String imagePath, String outputPath) {
|
||||
throw new UnsupportedOperationException("默认不支持该功能");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package cn.smartjavaai.ocr.recognition;
|
||||
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
* @date 2025/4/22
|
||||
*/
|
||||
@Data
|
||||
public class OcrRecModelConfig {
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
private OcrRecModelEnum modelEnum;
|
||||
|
||||
/**
|
||||
* 设备类型
|
||||
*/
|
||||
private DeviceEnum device;
|
||||
|
||||
/**
|
||||
* 模型路径
|
||||
*/
|
||||
private String modelPath;
|
||||
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package cn.smartjavaai.ocr.recognition;
|
||||
|
||||
import cn.smartjavaai.common.config.Config;
|
||||
import cn.smartjavaai.ocr.exception.OcrException;
|
||||
import cn.smartjavaai.ocr.ppv4.model.PaddleOCRV4DetModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* OCR模型工厂
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class OcrRecModelFactory {
|
||||
|
||||
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
|
||||
private static volatile OcrRecModelFactory instance;
|
||||
|
||||
private static final ConcurrentHashMap<String, OcrRecModel> modelMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 算法注册表
|
||||
*/
|
||||
private static final Map<String, Class<? extends OcrRecModel>> registry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public static OcrRecModelFactory getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (OcrRecModelFactory.class) {
|
||||
if (instance == null) {
|
||||
instance = new OcrRecModelFactory();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 注册算法
|
||||
* @param name
|
||||
* @param clazz
|
||||
*/
|
||||
private static void registerModel(String name, Class<? extends OcrRecModel> clazz) {
|
||||
registry.put(name.toLowerCase(), clazz);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public OcrRecModel getModel(OcrRecModelConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||
throw new OcrException("未配置OCR模型");
|
||||
}
|
||||
return modelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
|
||||
return createFaceModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用ModelConfig创建算法
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private OcrRecModel createFaceModel(OcrRecModelConfig config) {
|
||||
Class<?> clazz = registry.get(config.getModelEnum().name().toLowerCase());
|
||||
if(clazz == null){
|
||||
throw new OcrException("Unsupported model");
|
||||
}
|
||||
OcrRecModel algorithm = null;
|
||||
try {
|
||||
algorithm = (OcrRecModel) clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new OcrException(e);
|
||||
}
|
||||
algorithm.loadModel(config);
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
|
||||
// 初始化默认算法
|
||||
static {
|
||||
//registerModel("PADDLEOCR_V4_REC_MODEL", PaddleOCRV4DetModel.class);
|
||||
log.info("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
package cn.smartjavaai.ocr.utils;
|
||||
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.ImageFactory;
|
||||
import ai.djl.modality.cv.output.DetectedObjects;
|
||||
import ai.djl.ndarray.NDArray;
|
||||
import cn.smartjavaai.common.entity.DetectionInfo;
|
||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.utils.OpenCVUtils;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.Point;
|
||||
import org.opencv.core.Scalar;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图像工具类
|
||||
*/
|
||||
public class ImageUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 保存BufferedImage图片
|
||||
*
|
||||
* @param img
|
||||
* @param name
|
||||
* @param path
|
||||
*/
|
||||
public static void saveImage(BufferedImage img, String name, String path) {
|
||||
Image djlImg = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(img)); // 支持多种图片格式,自动适配
|
||||
Path outputDir = Paths.get(path);
|
||||
Path imagePath = outputDir.resolve(name);
|
||||
// OpenJDK 不能保存 jpg 图片的 alpha channel
|
||||
try {
|
||||
djlImg.save(Files.newOutputStream(imagePath), "png");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存DJL图片
|
||||
*
|
||||
* @param img
|
||||
* @param name
|
||||
* @param path
|
||||
*/
|
||||
public static void saveImage(Image img, String name, String path) {
|
||||
Path outputDir = Paths.get(path);
|
||||
Path imagePath = outputDir.resolve(name);
|
||||
// OpenJDK 不能保存 jpg 图片的 alpha channel
|
||||
try {
|
||||
img.save(Files.newOutputStream(imagePath), "png");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存图片,含检测框
|
||||
*
|
||||
* @param img
|
||||
* @param detection
|
||||
* @param name
|
||||
* @param path
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void saveBoundingBoxImage(
|
||||
Image img, DetectedObjects detection, String name, String path) throws IOException {
|
||||
// Make image copy with alpha channel because original image was jpg
|
||||
img.drawBoundingBoxes(detection);
|
||||
Path outputDir = Paths.get(path);
|
||||
Files.createDirectories(outputDir);
|
||||
Path imagePath = outputDir.resolve(name);
|
||||
// OpenJDK can't save jpg with alpha channel
|
||||
img.save(Files.newOutputStream(imagePath), "png");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 画检测框(有倾斜角)
|
||||
*
|
||||
* @param image
|
||||
* @param box
|
||||
*/
|
||||
public static void drawImageRect(BufferedImage image, NDArray box) {
|
||||
float[] points = box.toFloatArray();
|
||||
int[] xPoints = new int[5];
|
||||
int[] yPoints = new int[5];
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
xPoints[i] = (int) points[2 * i];
|
||||
yPoints[i] = (int) points[2 * i + 1];
|
||||
}
|
||||
xPoints[4] = xPoints[0];
|
||||
yPoints[4] = yPoints[0];
|
||||
|
||||
// 将绘制图像转换为Graphics2D
|
||||
Graphics2D g = (Graphics2D) image.getGraphics();
|
||||
try {
|
||||
g.setColor(new Color(0, 255, 0));
|
||||
// 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角
|
||||
BasicStroke bStroke = new BasicStroke(4, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
|
||||
g.setStroke(bStroke);
|
||||
g.drawPolyline(xPoints, yPoints, 5); // xPoints, yPoints, nPoints
|
||||
} finally {
|
||||
g.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 画检测框(有倾斜角)和文本
|
||||
*
|
||||
* @param image
|
||||
* @param box
|
||||
* @param text
|
||||
*/
|
||||
public static void drawImageRectWithText(BufferedImage image, NDArray box, String text) {
|
||||
float[] points = box.toFloatArray();
|
||||
int[] xPoints = new int[5];
|
||||
int[] yPoints = new int[5];
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
xPoints[i] = (int) points[2 * i];
|
||||
yPoints[i] = (int) points[2 * i + 1];
|
||||
}
|
||||
xPoints[4] = xPoints[0];
|
||||
yPoints[4] = yPoints[0];
|
||||
|
||||
// 将绘制图像转换为Graphics2D
|
||||
Graphics2D g = (Graphics2D) image.getGraphics();
|
||||
try {
|
||||
int fontSize = 32;
|
||||
Font font = new Font("楷体", Font.PLAIN, fontSize);
|
||||
g.setFont(font);
|
||||
g.setColor(new Color(0, 0, 255));
|
||||
// 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角
|
||||
BasicStroke bStroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
|
||||
g.setStroke(bStroke);
|
||||
g.drawPolyline(xPoints, yPoints, 5); // xPoints, yPoints, nPoints
|
||||
g.drawString(text, xPoints[0], yPoints[0]);
|
||||
} finally {
|
||||
g.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 画检测框
|
||||
*
|
||||
* @param image
|
||||
* @param x
|
||||
* @param y
|
||||
* @param width
|
||||
* @param height
|
||||
*/
|
||||
public static void drawImageRect(BufferedImage image, int x, int y, int width, int height) {
|
||||
// 将绘制图像转换为Graphics2D
|
||||
Graphics2D g = (Graphics2D) image.getGraphics();
|
||||
try {
|
||||
g.setColor(new Color(0, 255, 0));
|
||||
// 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角
|
||||
BasicStroke bStroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
|
||||
g.setStroke(bStroke);
|
||||
g.drawRect(x, y, width, height);
|
||||
} finally {
|
||||
g.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示文字
|
||||
*
|
||||
* @param image
|
||||
* @param text
|
||||
* @param x
|
||||
* @param y
|
||||
*/
|
||||
public static void drawImageText(BufferedImage image, String text, int x, int y) {
|
||||
Graphics graphics = image.getGraphics();
|
||||
int fontSize = 32;
|
||||
Font font = new Font("楷体", Font.PLAIN, fontSize);
|
||||
try {
|
||||
graphics.setFont(font);
|
||||
graphics.setColor(new Color(0, 0, 255));
|
||||
int strWidth = graphics.getFontMetrics().stringWidth(text);
|
||||
graphics.drawString(text, x, y);
|
||||
} finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 画矩形
|
||||
*
|
||||
* @param mat
|
||||
* @param box
|
||||
*/
|
||||
public static void drawRect(Mat mat, DetectionResponse detectionResponse) {
|
||||
|
||||
for(DetectionInfo detectionInfo : detectionResponse.getDetectionInfoList()){
|
||||
DetectionRectangle detectionRectangle = detectionInfo.getDetectionRectangle();
|
||||
// 左上角点
|
||||
Point topLeft = new Point(detectionRectangle.getX(), detectionRectangle.getY());
|
||||
// 右下角点
|
||||
Point bottomRight = new Point(detectionRectangle.getX() + detectionRectangle.getWidth(), detectionRectangle.getY() + detectionRectangle.getHeight());
|
||||
// 绘制矩形(图像、左上角、右下角、颜色(BGR)、线宽)
|
||||
Imgproc.rectangle(mat, topLeft, bottomRight, new Scalar(0, 255, 0), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,30 @@
|
||||
package cn.smartjavaai.ocr.utils;
|
||||
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.ImageFactory;
|
||||
import ai.djl.modality.cv.output.BoundingBox;
|
||||
import ai.djl.modality.cv.output.DetectedObjects;
|
||||
import ai.djl.modality.cv.util.NDImageUtils;
|
||||
import ai.djl.ndarray.NDArray;
|
||||
import ai.djl.ndarray.NDList;
|
||||
import cn.smartjavaai.common.entity.DetectionInfo;
|
||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import ai.djl.ndarray.NDManager;
|
||||
import ai.djl.opencv.OpenCVImageFactory;
|
||||
import cn.smartjavaai.common.entity.*;
|
||||
import cn.smartjavaai.common.entity.Point;
|
||||
import cn.smartjavaai.ocr.entity.OcrBox;
|
||||
import cn.smartjavaai.ocr.entity.OcrInfo;
|
||||
import cn.smartjavaai.ocr.entity.OcrItem;
|
||||
import cn.smartjavaai.ocr.entity.RotatedBoxCompX;
|
||||
import cn.smartjavaai.ocr.enums.AngleEnum;
|
||||
import cn.smartjavaai.ocr.opencv.OcrNDArrayUtils;
|
||||
import cn.smartjavaai.ocr.opencv.OcrOpenCVUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.core.Scalar;
|
||||
import org.opencv.imgproc.Imgproc;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
@@ -25,40 +40,243 @@ public class OcrUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 转换为FaceDetectedResult
|
||||
* 转换为OcrBox
|
||||
* @param dt_boxes
|
||||
* @param img
|
||||
* @return
|
||||
*/
|
||||
public static DetectionResponse convertToDetectionResponse(NDList dt_boxes, Image img){
|
||||
public static List<OcrBox> convertToOcrBox(NDList dt_boxes, Image img){
|
||||
if(Objects.isNull(dt_boxes) || dt_boxes.size() == 0){
|
||||
return null;
|
||||
}
|
||||
DetectionResponse detectionResponse = new DetectionResponse();
|
||||
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
|
||||
List<OcrBox> boxList = new ArrayList<OcrBox>();
|
||||
for(NDArray box : dt_boxes){
|
||||
DetectionRectangle rectangle = new DetectionRectangle();
|
||||
float[] points = box.toFloatArray();
|
||||
log.info("points: {}", points);
|
||||
int x = (int)points[0];
|
||||
int y = (int)points[1];
|
||||
int width = new BigDecimal(points[4]).subtract(new BigDecimal(points[6])).intValue();
|
||||
int height = new BigDecimal(points[7]).subtract(new BigDecimal(points[1])).intValue();
|
||||
|
||||
// 修正边界,防止越界
|
||||
if (x < 0) x = 0;
|
||||
if (y < 0) y = 0;
|
||||
if (x + width > img.getWidth()) width = img.getWidth() - x;
|
||||
if (y + height > img.getHeight()) height = img.getHeight() - y;
|
||||
|
||||
rectangle.setX(x);
|
||||
rectangle.setY(y);
|
||||
rectangle.setHeight(height);
|
||||
rectangle.setWidth(width);
|
||||
detectionInfoList.add(new DetectionInfo(rectangle));
|
||||
float[] pointsArr = box.toFloatArray();
|
||||
//log.info("points: {}", pointsArr);
|
||||
float[] lt = java.util.Arrays.copyOfRange(pointsArr, 0, 2);
|
||||
float[] rt = java.util.Arrays.copyOfRange(pointsArr, 2, 4);
|
||||
float[] rb = java.util.Arrays.copyOfRange(pointsArr, 4, 6);
|
||||
float[] lb = java.util.Arrays.copyOfRange(pointsArr, 6, 8);
|
||||
OcrBox ocrBox = new OcrBox(new Point(lt[0], lt[1]), new Point(rt[0], rt[1]), new Point(rb[0], rb[1]), new Point(lb[0], lb[1]));
|
||||
boxList.add(ocrBox);
|
||||
}
|
||||
return boxList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 欧式距离计算
|
||||
*
|
||||
* @param point1
|
||||
* @param point2
|
||||
* @return
|
||||
*/
|
||||
public static float distance(float[] point1, float[] point2) {
|
||||
float disX = point1[0] - point2[0];
|
||||
float disY = point1[1] - point2[1];
|
||||
float dis = (float) Math.sqrt(disX * disX + disY * disY);
|
||||
return dis;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片旋转
|
||||
*
|
||||
* @param manager
|
||||
* @param image
|
||||
* @return
|
||||
*/
|
||||
public static Image rotateImg(NDManager manager, Image image) {
|
||||
NDArray rotated = NDImageUtils.rotate90(image.toNDArray(manager), 1);
|
||||
return ImageFactory.getInstance().fromNDArray(rotated);
|
||||
}
|
||||
|
||||
/**
|
||||
* 逆时针旋转图片
|
||||
*
|
||||
* @param image
|
||||
* @param times
|
||||
* @return
|
||||
*/
|
||||
public static Image rotateImg(Image image, int times) {
|
||||
try (NDManager manager = NDManager.newBaseManager()) {
|
||||
NDArray rotated = NDImageUtils.rotate90(image.toNDArray(manager), times);
|
||||
return OpenCVImageFactory.getInstance().fromNDArray(rotated);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 逆时针旋转图片
|
||||
*
|
||||
* @param image
|
||||
* @param angleEnum
|
||||
* @return
|
||||
*/
|
||||
public static Image rotateImg(Image image, AngleEnum angleEnum) {
|
||||
try (NDManager manager = NDManager.newBaseManager()) {
|
||||
int times = 0;
|
||||
switch (angleEnum) {
|
||||
case ANGLE_90:
|
||||
times = 1;
|
||||
break;
|
||||
case ANGLE_180:
|
||||
times = 2;
|
||||
break;
|
||||
case ANGLE_270:
|
||||
times = 3;
|
||||
break;
|
||||
}
|
||||
NDArray rotated = NDImageUtils.rotate90(image.toNDArray(manager), times);
|
||||
return OpenCVImageFactory.getInstance().fromNDArray(rotated);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 转换为OcrInfo
|
||||
* @param lines
|
||||
* @return
|
||||
*/
|
||||
public static OcrInfo convertToOcrInfo(List<ArrayList<RotatedBoxCompX>> lines){
|
||||
if(Objects.isNull(lines) || lines.size() == 0){
|
||||
return null;
|
||||
}
|
||||
List<DetectionInfo> detectionInfoList = new ArrayList<DetectionInfo>();
|
||||
List<List<OcrItem>> lineList = new ArrayList<List<OcrItem>>();
|
||||
String fullText = "";
|
||||
for(ArrayList<RotatedBoxCompX> boxList : lines){
|
||||
List<OcrItem> line = new ArrayList<OcrItem>();
|
||||
for(RotatedBoxCompX box : boxList){
|
||||
float[] pointsArr = box.getBox().toFloatArray();
|
||||
float[] lt = java.util.Arrays.copyOfRange(pointsArr, 0, 2);
|
||||
float[] rt = java.util.Arrays.copyOfRange(pointsArr, 2, 4);
|
||||
float[] rb = java.util.Arrays.copyOfRange(pointsArr, 4, 6);
|
||||
float[] lb = java.util.Arrays.copyOfRange(pointsArr, 6, 8);
|
||||
OcrBox ocrBox = new OcrBox(new Point(lt[0], lt[1]), new Point(rt[0], rt[1]), new Point(rb[0], rb[1]), new Point(lb[0], lb[1]));
|
||||
OcrItem ocrItem = new OcrItem(ocrBox, box.getText());
|
||||
line.add(ocrItem);
|
||||
String text = box.getText();
|
||||
if(text.trim().equals(""))
|
||||
continue;
|
||||
fullText += text + " ";
|
||||
}
|
||||
lineList.add(line);
|
||||
fullText += '\n';
|
||||
}
|
||||
return new OcrInfo(lineList, fullText);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 放射变换+裁剪
|
||||
* @param srcMat
|
||||
* @param box
|
||||
* @return
|
||||
*/
|
||||
public static Image transformAndCrop(Mat srcMat, OcrBox box){
|
||||
float[] pointsArr = box.toFloatArray();
|
||||
float[] lt = java.util.Arrays.copyOfRange(pointsArr, 0, 2);
|
||||
float[] rt = java.util.Arrays.copyOfRange(pointsArr, 2, 4);
|
||||
float[] rb = java.util.Arrays.copyOfRange(pointsArr, 4, 6);
|
||||
float[] lb = java.util.Arrays.copyOfRange(pointsArr, 6, 8);
|
||||
int img_crop_width = (int) Math.max(OcrUtils.distance(lt, rt), OcrUtils.distance(rb, lb));
|
||||
int img_crop_height = (int) Math.max(OcrUtils.distance(lt, lb), OcrUtils.distance(rt, rb));
|
||||
List<ai.djl.modality.cv.output.Point> srcPoints = new ArrayList<>();
|
||||
srcPoints.add(new ai.djl.modality.cv.output.Point(lt[0], lt[1]));
|
||||
srcPoints.add(new ai.djl.modality.cv.output.Point(rt[0], rt[1]));
|
||||
srcPoints.add(new ai.djl.modality.cv.output.Point(rb[0], rb[1]));
|
||||
srcPoints.add(new ai.djl.modality.cv.output.Point(lb[0], lb[1]));
|
||||
List<ai.djl.modality.cv.output.Point> dstPoints = new ArrayList<>();
|
||||
dstPoints.add(new ai.djl.modality.cv.output.Point(0, 0));
|
||||
dstPoints.add(new ai.djl.modality.cv.output.Point(img_crop_width, 0));
|
||||
dstPoints.add(new ai.djl.modality.cv.output.Point(img_crop_width, img_crop_height));
|
||||
dstPoints.add(new ai.djl.modality.cv.output.Point(0, img_crop_height));
|
||||
Mat srcPoint2f = OcrNDArrayUtils.toMat(srcPoints);
|
||||
Mat dstPoint2f = OcrNDArrayUtils.toMat(dstPoints);
|
||||
//透视变换
|
||||
Mat cvMat = OcrOpenCVUtils.perspectiveTransform(srcMat, srcPoint2f, dstPoint2f);
|
||||
Image subImg = OpenCVImageFactory.getInstance().fromImage(cvMat);
|
||||
//ImageUtils.saveImage(subImg, i + ".png", "build/output");
|
||||
//变换后裁剪
|
||||
subImg = subImg.getSubImage(0, 0, img_crop_width, img_crop_height);
|
||||
cvMat.release();
|
||||
srcPoint2f.release();
|
||||
dstPoint2f.release();
|
||||
return subImg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制文本框
|
||||
*
|
||||
* @param mat
|
||||
* @param boxList
|
||||
*/
|
||||
public static void drawRect(Mat mat, List<OcrBox> boxList) {
|
||||
for(OcrBox ocrBox : boxList){
|
||||
Imgproc.line(mat, ocrBox.getTopLeft().toCvPoint(), ocrBox.getTopRight().toCvPoint(), new Scalar(0, 255, 0), 1);
|
||||
Imgproc.line(mat, ocrBox.getTopRight().toCvPoint(), ocrBox.getBottomRight().toCvPoint(), new Scalar(0, 255, 0),1);
|
||||
Imgproc.line(mat, ocrBox.getBottomRight().toCvPoint(), ocrBox.getBottomLeft().toCvPoint(), new Scalar(0, 255, 0),1);
|
||||
Imgproc.line(mat, ocrBox.getBottomLeft().toCvPoint(), ocrBox.getTopLeft().toCvPoint(), new Scalar(0, 255, 0), 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 绘制文本框及文本
|
||||
* @param image
|
||||
* @param ocrInfo
|
||||
*/
|
||||
public static void drawRectWithText(BufferedImage image, OcrInfo ocrInfo, int fontSize) {
|
||||
// 将绘制图像转换为Graphics2D
|
||||
Graphics2D g = (Graphics2D) image.getGraphics();
|
||||
try {
|
||||
Font font = new Font("楷体", Font.PLAIN, fontSize);
|
||||
g.setFont(font);
|
||||
g.setColor(new Color(0, 0, 255));
|
||||
// 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角
|
||||
BasicStroke bStroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
|
||||
g.setStroke(bStroke);
|
||||
for(List<OcrItem> ocrItemList : ocrInfo.getLineList()){
|
||||
for(OcrItem item : ocrItemList){
|
||||
OcrBox box = item.getOcrBox();
|
||||
int[] xPoints = {
|
||||
(int)box.getTopLeft().getX(),
|
||||
(int)box.getTopRight().getX(),
|
||||
(int)box.getBottomRight().getX(),
|
||||
(int)box.getBottomLeft().getX(),
|
||||
(int)box.getTopLeft().getX()
|
||||
};
|
||||
int[] yPoints = {
|
||||
(int)box.getTopLeft().getY(),
|
||||
(int)box.getTopRight().getY(),
|
||||
(int)box.getBottomRight().getY(),
|
||||
(int)box.getBottomLeft().getY(),
|
||||
(int)box.getTopLeft().getY()
|
||||
};
|
||||
g.drawPolyline(xPoints, yPoints, 5);
|
||||
g.drawString(item.getText(), xPoints[0], yPoints[0]);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
g.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 绘制文本框及文本
|
||||
* @param srcMat
|
||||
* @param itemList
|
||||
*/
|
||||
public static void drawRectWithText(Mat srcMat, List<OcrItem> itemList) {
|
||||
for(OcrItem item : itemList){
|
||||
OcrBox ocrBox = item.getOcrBox();
|
||||
Imgproc.line(srcMat, ocrBox.getTopLeft().toCvPoint(), ocrBox.getTopRight().toCvPoint(), new Scalar(0, 255, 0), 1);
|
||||
Imgproc.line(srcMat, ocrBox.getTopRight().toCvPoint(), ocrBox.getBottomRight().toCvPoint(), new Scalar(0, 255, 0),1);
|
||||
Imgproc.line(srcMat, ocrBox.getBottomRight().toCvPoint(), ocrBox.getBottomLeft().toCvPoint(), new Scalar(0, 255, 0),1);
|
||||
Imgproc.line(srcMat, ocrBox.getBottomLeft().toCvPoint(), ocrBox.getTopLeft().toCvPoint(), new Scalar(0, 255, 0), 1);
|
||||
// 中文乱码
|
||||
Imgproc.putText(srcMat, item.getAngle().getValue(), ocrBox.getTopLeft().toCvPoint(), Imgproc.FONT_HERSHEY_SCRIPT_SIMPLEX, 1.0, new Scalar(0, 255, 0), 1);
|
||||
}
|
||||
detectionResponse.setDetectionInfoList(detectionInfoList);
|
||||
return detectionResponse;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user