mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-09-12 12:48:57 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dea0673026 |
84
README.md
84
README.md
@@ -6,7 +6,7 @@
|
|||||||
<em>像Hutool一样简单易用的Java AI工具箱</em>
|
<em>像Hutool一样简单易用的Java AI工具箱</em>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
👉 <a href="http://numberone.ink/">http://numberone.ink/</a> 👈
|
👉 <a href="http://smartjavaai.cn/">http://smartjavaai.cn/</a> 👈
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a target="_blank" href="https://central.sonatype.com/artifact/ink.numberone/smartjavaai-all">
|
<a target="_blank" href="https://central.sonatype.com/artifact/ink.numberone/smartjavaai-all">
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
[**开发文档**](http://doc.numberone.ink)
|
[**开发文档**](http://doc.smartjavaai.cn)
|
||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/face/face4.jpg" width = "400px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/face5point.png" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -81,7 +81,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/face1-1.jpg" width = "400px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/face1-1.jpg" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -93,7 +93,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/idcard.png" width = "400px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/idcard.png" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -109,7 +109,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/face1-n.png" width = "400px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/face1-n.png" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -126,7 +126,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/face_attribute.jpg" width = "400px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/face_attribute.jpg" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -139,7 +139,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/liveness2.jpg" width = "400px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/liveness2.jpg" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -152,7 +152,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/emotion.jpg" width = "400px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/emotion.jpg" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -177,7 +177,12 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/objectdect/object_detection_detected.jpg" width = "400px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/objectdect/object_detect_1.jpeg" width = "500px"/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div align="center">
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/objectdect/object_detection_detected.jpg" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -289,7 +294,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/ocr/general_ocr_002_recognized.png" width = "400px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/ocr/general_ocr_002_recognized.png" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -302,7 +307,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/ocr/table.jpg" width = "400px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/ocr/table.jpg" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -316,44 +321,14 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/ocr/plate_recognized.jpg" width = "400px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/ocr/plate_recognized.jpg" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
<td>
|
||||||
<div align="left">
|
<div align="center">
|
||||||
<p>身份证识别<br>(IDCard Recognition)</p>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/ocr/plate_recognized2.jpg" width = "500px"/>
|
||||||
- 正面识别 <br>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
<div align="center">
|
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/ocr/idcard/idcard_front.png" width="500px"/>
|
|
||||||
<pre align="left">{
|
|
||||||
"name": "小氧",
|
|
||||||
"gender": "女",
|
|
||||||
"ethnicity": "汉",
|
|
||||||
"idNumber": "430602200010108888",
|
|
||||||
"birthday": "2000-10-10",
|
|
||||||
"address": "湖南省岳阳市岳阳楼区金鄂中路456号"
|
|
||||||
}</pre>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div align="left">
|
|
||||||
<p>身份证识别<br>(IDCard Recognition)</p>
|
|
||||||
- 反面识别 <br>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div align="center">
|
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/ocr/idcard/idcard_back.png" width="500px"/>
|
|
||||||
<pre align="left">{"issuingAuthority":"杭州市公安局江干分局","validFrom":"2015-11-05","validTo":"2025-11-05"}</pre>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -364,7 +339,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/translate/translate.png" width = "400px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/translate/translate.png" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -378,7 +353,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/speech/asr.png" width = "400px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/speech/asr.png" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -390,7 +365,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/speech/tts.jpg" width = "400px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/speech/tts.jpg" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -405,7 +380,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/vision/clip.png" width = "400px"/>
|
<img src="https://cdn.jsdelivr.net/gh/geekwenjie/SmartJavaAI-Site/images/vision/clip.png" width = "500px"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -472,7 +447,6 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
- 支持通用文字识别,通用手写字识别
|
- 支持通用文字识别,通用手写字识别
|
||||||
- 支持表格识别
|
- 支持表格识别
|
||||||
- 支持中文车牌识别:单层/双层检测,颜色识别,支持12种中文车牌
|
- 支持中文车牌识别:单层/双层检测,颜色识别,支持12种中文车牌
|
||||||
- 支持身份证识别:支持身份证正反面字段提取、方向矫正与结构化解析
|
|
||||||
- **机器翻译**
|
- **机器翻译**
|
||||||
- 集成NLLB-200模型:支持200+语言互相翻译
|
- 集成NLLB-200模型:支持200+语言互相翻译
|
||||||
- **语音识别(ASR)**
|
- **语音识别(ASR)**
|
||||||
@@ -526,8 +500,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
### 1、环境要求
|
### 1、环境要求
|
||||||
|
|
||||||
- Java 版本:**JDK 8或更高版本**
|
- Java 版本:**JDK 8或更高版本**
|
||||||
- 操作系统:Windows 64 位 / Linux / macOS M1
|
- 操作系统:不同模型支持的系统不一样,具体请查看[文档](http://doc.smartjavaai.cn)
|
||||||
- CPU架构:x86_64、ARM64(aarch64)
|
|
||||||
|
|
||||||
### 2、Maven
|
### 2、Maven
|
||||||
|
|
||||||
@@ -535,13 +508,13 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
|
|
||||||
⚠️ **注意:不推荐直接引入全部依赖**,更推荐根据实际需求,按功能模块单独引入,避免引入不必要的包。
|
⚠️ **注意:不推荐直接引入全部依赖**,更推荐根据实际需求,按功能模块单独引入,避免引入不必要的包。
|
||||||
|
|
||||||
详细引入方式请查看 [文档](http://doc.numberone.ink/install.html)、或查看[示例代码](https://gitee.com/dengwenjie/SmartJavaAI/tree/master/examples)
|
详细引入方式请查看 [文档](http://doc.smartjavaai.cn/install.html)、或查看[示例代码](https://gitee.com/dengwenjie/SmartJavaAI/tree/master/examples)
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>all</artifactId>
|
<artifactId>all</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -575,7 +548,7 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
|
|
||||||
### 4、文档地址
|
### 4、文档地址
|
||||||
|
|
||||||
[开发文档](http://doc.numberone.ink)
|
[开发文档](http://doc.smartjavaai.cn)
|
||||||
|
|
||||||
### 5、模型简介及下载
|
### 5、模型简介及下载
|
||||||
|
|
||||||
@@ -903,3 +876,4 @@ SmartJavaAI是专为JAVA 开发者打造的一个功能丰富、开箱即用的
|
|||||||
6、等待维护者合并
|
6、等待维护者合并
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-parent</artifactId>
|
<artifactId>smartjavaai-parent</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>all</artifactId>
|
<artifactId>all</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
<name>${project.artifactId}</name>
|
<name>${project.artifactId}</name>
|
||||||
<description>SmartJavaAI</description>
|
<description>SmartJavaAI</description>
|
||||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-parent</artifactId>
|
<artifactId>smartjavaai-parent</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
<artifactId>bom</artifactId>
|
<artifactId>bom</artifactId>
|
||||||
<name>bom</name>
|
<name>bom</name>
|
||||||
<description>统一版本管理的 BOM 包,同时支持 import 和全量依赖</description>
|
<description>统一版本管理的 BOM 包,同时支持 import 和全量依赖</description>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-parent</artifactId>
|
<artifactId>smartjavaai-parent</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<name>common</name>
|
<name>common</name>
|
||||||
|
|||||||
@@ -59,6 +59,6 @@ src/main/java/smartai/examples/face/
|
|||||||
## 📄 文档
|
## 📄 文档
|
||||||
|
|
||||||
有关完整使用说明,请查阅 SmartJavaAI 官方文档:
|
有关完整使用说明,请查阅 SmartJavaAI 官方文档:
|
||||||
[http://doc.numberone.ink](http://doc.numberone.ink)
|
[http://doc.smartjavaai.cn](http://doc.smartjavaai.cn)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<maven.compiler.source>11</maven.compiler.source>
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
<maven.compiler.target>11</maven.compiler.target>
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<smartjavaai.version>1.1.2</smartjavaai.version>
|
<smartjavaai.version>1.1.1</smartjavaai.version>
|
||||||
<!--如果打包运行,需要替换成你的main-->
|
<!--如果打包运行,需要替换成你的main-->
|
||||||
<exec.mainClass>smartai.examples.face.FaceDemo</exec.mainClass>
|
<exec.mainClass>smartai.examples.face.FaceDemo</exec.mainClass>
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* 人脸属性检测demo
|
* 人脸属性检测demo
|
||||||
* 模型下载地址:https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234 提取码: 1234
|
||||||
* 文档地址:http://doc.numberone.ink/
|
* 文档地址:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import java.util.List;
|
|||||||
* 表情识别demo
|
* 表情识别demo
|
||||||
* 支持识别7种表情:neutral(中性)、happy(高兴)、sad(悲伤)、surprise(惊讶)、fear(恐惧)、disgust(厌恶)、anger(愤怒)
|
* 支持识别7种表情:neutral(中性)、happy(高兴)、sad(悲伤)、surprise(惊讶)、fear(恐惧)、disgust(厌恶)、anger(愤怒)
|
||||||
* 模型下载地址:https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234 提取码: 1234
|
||||||
* 文档地址:http://doc.numberone.ink/
|
* 文档地址:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import java.nio.file.Paths;
|
|||||||
/**
|
/**
|
||||||
* 人脸检测模型demo
|
* 人脸检测模型demo
|
||||||
* 模型下载地址:https://pan.baidu.com/s/1d2YlJ2YOdGn3Y-AegyAhmQ?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/1d2YlJ2YOdGn3Y-AegyAhmQ?pwd=1234 提取码: 1234
|
||||||
* 文档地址:http://doc.numberone.ink/
|
* 文档地址:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* 人脸识别模型demo
|
* 人脸识别模型demo
|
||||||
* 模型下载地址:https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234 提取码: 1234
|
||||||
* 文档地址:http://doc.numberone.ink/
|
* 文档地址:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -78,7 +78,7 @@ public class FaceRecDemo {
|
|||||||
* 获取人脸检测模型(高精度模型)
|
* 获取人脸检测模型(高精度模型)
|
||||||
* 注意事项:
|
* 注意事项:
|
||||||
* 1、高精度模型,识别准确度高,速度慢
|
* 1、高精度模型,识别准确度高,速度慢
|
||||||
* 2、具体其他模型参数可以查看文档:http://doc.numberone.ink/face.html
|
* 2、具体其他模型参数可以查看文档:http://doc.smartjavaai.cn/face.html
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public FaceDetModel getProFaceDetModel(){
|
public FaceDetModel getProFaceDetModel(){
|
||||||
@@ -98,7 +98,7 @@ public class FaceRecDemo {
|
|||||||
* 获取人脸检测模型(极速模型)
|
* 获取人脸检测模型(极速模型)
|
||||||
* 注意事项:
|
* 注意事项:
|
||||||
* 1、极速模型,识别准确度低,速度快
|
* 1、极速模型,识别准确度低,速度快
|
||||||
* 2、具体其他模型参数可以查看文档:http://doc.numberone.ink/face.html
|
* 2、具体其他模型参数可以查看文档:http://doc.smartjavaai.cn/face.html
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public FaceDetModel getFastFaceDetModel(){
|
public FaceDetModel getFastFaceDetModel(){
|
||||||
@@ -118,7 +118,7 @@ public class FaceRecDemo {
|
|||||||
/**
|
/**
|
||||||
* 获取人脸识别模型(高精度,速度慢)
|
* 获取人脸识别模型(高精度,速度慢)
|
||||||
* 追求准确度可以使用
|
* 追求准确度可以使用
|
||||||
* 也可以使用其他模型,具体其他模型参数可以查看文档:http://doc.numberone.ink/face.html
|
* 也可以使用其他模型,具体其他模型参数可以查看文档:http://doc.smartjavaai.cn/face.html
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public FaceRecModel getFaceRecModel(){
|
public FaceRecModel getFaceRecModel(){
|
||||||
@@ -140,7 +140,7 @@ public class FaceRecDemo {
|
|||||||
/**
|
/**
|
||||||
* 获取人脸识别模型(高速模型,精度一般)
|
* 获取人脸识别模型(高速模型,精度一般)
|
||||||
* 追求速度可以使用
|
* 追求速度可以使用
|
||||||
* 也可以使用其他模型,具体其他模型参数可以查看文档:http://doc.numberone.ink/face.html
|
* 也可以使用其他模型,具体其他模型参数可以查看文档:http://doc.smartjavaai.cn/face.html
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public FaceRecModel getHighSpeedFaceRecModel(){
|
public FaceRecModel getHighSpeedFaceRecModel(){
|
||||||
@@ -165,7 +165,7 @@ public class FaceRecDemo {
|
|||||||
*/
|
*/
|
||||||
public FaceRecModel getFaceRecModelWithDbConfig(){
|
public FaceRecModel getFaceRecModelWithDbConfig(){
|
||||||
FaceRecConfig config = new FaceRecConfig();
|
FaceRecConfig config = new FaceRecConfig();
|
||||||
//高精度模型,速度慢,追求速度请更换高速模型,具体其他模型参数可以查看文档:http://doc.numberone.ink/face.html
|
//高精度模型,速度慢,追求速度请更换高速模型,具体其他模型参数可以查看文档:http://doc.smartjavaai.cn/face.html
|
||||||
config.setModelEnum(FaceRecModelEnum.INSIGHT_FACE_IRSE50_MODEL);
|
config.setModelEnum(FaceRecModelEnum.INSIGHT_FACE_IRSE50_MODEL);
|
||||||
//模型路径,请下载模型并替换为本地路径:https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234 提取码: 1234
|
//模型路径,请下载模型并替换为本地路径:https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234 提取码: 1234
|
||||||
config.setModelPath("/Users/wenjie/Documents/develop/model/face_model/recognition/InsightFace/model_ir_se50.pt");
|
config.setModelPath("/Users/wenjie/Documents/develop/model/face_model/recognition/InsightFace/model_ir_se50.pt");
|
||||||
@@ -173,7 +173,7 @@ public class FaceRecDemo {
|
|||||||
config.setCropFace(true);
|
config.setCropFace(true);
|
||||||
//开启人脸对齐:适用于人脸不正的场景,开启将提升人脸特征准确度,关闭可以提升性能
|
//开启人脸对齐:适用于人脸不正的场景,开启将提升人脸特征准确度,关闭可以提升性能
|
||||||
config.setAlign(true);
|
config.setAlign(true);
|
||||||
//指定人脸检测模型,可切换人脸检测模型(极速:getFastFaceDetModel,高精度:getProFaceDetModel),具体其他模型参数可以查看文档:http://doc.numberone.ink/face.html
|
//指定人脸检测模型,可切换人脸检测模型(极速:getFastFaceDetModel,高精度:getProFaceDetModel),具体其他模型参数可以查看文档:http://doc.smartjavaai.cn/face.html
|
||||||
config.setDetectModel(getFaceDetModel());
|
config.setDetectModel(getFaceDetModel());
|
||||||
config.setDevice(device);
|
config.setDevice(device);
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ public class FaceRecDemo {
|
|||||||
*/
|
*/
|
||||||
public FaceRecModel getFaceRecModelWithSQLiteConfig(){
|
public FaceRecModel getFaceRecModelWithSQLiteConfig(){
|
||||||
FaceRecConfig config = new FaceRecConfig();
|
FaceRecConfig config = new FaceRecConfig();
|
||||||
//高精度模型,速度慢, 追求速度请更换高速模型,具体其他模型参数可以查看文档:http://doc.numberone.ink/face.html
|
//高精度模型,速度慢, 追求速度请更换高速模型,具体其他模型参数可以查看文档:http://doc.smartjavaai.cn/face.html
|
||||||
config.setModelEnum(FaceRecModelEnum.INSIGHT_FACE_IRSE50_MODEL);
|
config.setModelEnum(FaceRecModelEnum.INSIGHT_FACE_IRSE50_MODEL);
|
||||||
//模型路径,请下载模型并替换为本地路径:https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234 提取码: 1234
|
//模型路径,请下载模型并替换为本地路径:https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234 提取码: 1234
|
||||||
config.setModelPath("/Users/wenjie/Documents/develop/model/face_model/recognition/InsightFace/model_ir_se50.pt");
|
config.setModelPath("/Users/wenjie/Documents/develop/model/face_model/recognition/InsightFace/model_ir_se50.pt");
|
||||||
@@ -206,7 +206,7 @@ public class FaceRecDemo {
|
|||||||
config.setCropFace(true);
|
config.setCropFace(true);
|
||||||
//开启人脸对齐:适用于人脸不正的场景,开启将提升人脸特征准确度,关闭可以提升性能
|
//开启人脸对齐:适用于人脸不正的场景,开启将提升人脸特征准确度,关闭可以提升性能
|
||||||
config.setAlign(true);
|
config.setAlign(true);
|
||||||
//指定人脸检测模型,可切换人脸检测模型(极速:getFastFaceDetModel,高精度:getProFaceDetModel),具体其他模型参数可以查看文档:http://doc.numberone.ink/face.html
|
//指定人脸检测模型,可切换人脸检测模型(极速:getFastFaceDetModel,高精度:getProFaceDetModel),具体其他模型参数可以查看文档:http://doc.smartjavaai.cn/face.html
|
||||||
config.setDetectModel(getFaceDetModel());
|
config.setDetectModel(getFaceDetModel());
|
||||||
config.setDevice(device);
|
config.setDevice(device);
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* 静态活体检测demo
|
* 静态活体检测demo
|
||||||
* 模型下载地址:https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234 提取码: 1234
|
||||||
* 文档地址:http://doc.numberone.ink/
|
* 文档地址:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import java.nio.file.Paths;
|
|||||||
/**
|
/**
|
||||||
* 人脸质量评估 demo
|
* 人脸质量评估 demo
|
||||||
* 模型下载地址:https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/10l22x5fRz_gwLr8EAHa1Jg?pwd=1234 提取码: 1234
|
||||||
* 文档地址:http://doc.numberone.ink/
|
* 文档地址:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -61,6 +61,6 @@ src
|
|||||||
## 📄 文档
|
## 📄 文档
|
||||||
|
|
||||||
有关完整使用说明,请查阅 SmartJavaAI 官方文档:
|
有关完整使用说明,请查阅 SmartJavaAI 官方文档:
|
||||||
[http://doc.numberone.ink](http://doc.numberone.ink)
|
[http://doc.smartjavaai.cn](http://doc.smartjavaai.cn)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
<maven.compiler.source>11</maven.compiler.source>
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
<maven.compiler.target>11</maven.compiler.target>
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<smartjavaai.version>1.1.2</smartjavaai.version>
|
<smartjavaai.version>1.1.1</smartjavaai.version>
|
||||||
<!--如果打包运行,需要替换成你的main-->
|
<!--如果打包运行,需要替换成你的main-->
|
||||||
<exec.mainClass>smartai.examples.ocr.common.OcrDetectionDemo</exec.mainClass>
|
<exec.mainClass>smartai.examples.ocr.common.OcrRecognizeDemo</exec.mainClass>
|
||||||
|
|
||||||
<javacv.version>1.5.10</javacv.version>
|
<javacv.version>1.5.10</javacv.version>
|
||||||
|
|
||||||
@@ -90,33 +90,19 @@
|
|||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>ocr</artifactId>
|
<artifactId>ocr</artifactId>
|
||||||
<exclusions>
|
<exclusions>
|
||||||
<!-- <exclusion>-->
|
<exclusion>
|
||||||
<!-- <groupId>org.openpnp</groupId>-->
|
<groupId>com.microsoft.onnxruntime</groupId>
|
||||||
<!-- <artifactId>opencv</artifactId>-->
|
<artifactId>onnxruntime</artifactId>
|
||||||
<!-- </exclusion>-->
|
</exclusion>
|
||||||
<!-- <exclusion>-->
|
|
||||||
<!-- <groupId>com.microsoft.onnxruntime</groupId>-->
|
|
||||||
<!-- <artifactId>onnxruntime</artifactId>-->
|
|
||||||
<!-- </exclusion>-->
|
|
||||||
<!-- <exclusion>-->
|
|
||||||
<!-- <groupId>org.bytedeco</groupId>-->
|
|
||||||
<!-- <artifactId>javacv</artifactId>-->
|
|
||||||
<!-- </exclusion>-->
|
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- <dependency>-->
|
<dependency>
|
||||||
<!-- <groupId>com.microsoft.onnxruntime</groupId>-->
|
<groupId>com.microsoft.onnxruntime</groupId>
|
||||||
<!-- <artifactId>onnxruntime</artifactId>-->
|
<artifactId>onnxruntime</artifactId>
|
||||||
<!-- <version>1.16.3</version>-->
|
<version>1.20.0</version>
|
||||||
<!-- <scope>compile</scope>-->
|
<scope>runtime</scope>
|
||||||
<!-- </dependency>-->
|
</dependency>
|
||||||
|
|
||||||
<!-- <dependency>-->
|
|
||||||
<!-- <groupId>org.openpnp</groupId>-->
|
|
||||||
<!-- <artifactId>opencv</artifactId>-->
|
|
||||||
<!-- <version>3.4.2-2</version>-->
|
|
||||||
<!-- </dependency>-->
|
|
||||||
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -127,10 +113,79 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- windows平台 (保留对应平台的配置,可以减小包大小)-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>javacpp</artifactId>
|
||||||
|
<version>${javacv.version}</version>
|
||||||
|
<classifier>${javacv.platform.windows-x86_64}</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>ffmpeg</artifactId>
|
||||||
|
<version>6.1.1-1.5.10</version>
|
||||||
|
<classifier>${javacv.platform.windows-x86_64}</classifier>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>openblas</artifactId>
|
||||||
|
<version>0.3.26-1.5.10</version>
|
||||||
|
<classifier>${javacv.platform.windows-x86_64}</classifier>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>opencv</artifactId>
|
||||||
|
<version>4.9.0-1.5.10</version>
|
||||||
|
<classifier>${javacv.platform.windows-x86_64}</classifier>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>ai.djl.pytorch</groupId>
|
||||||
|
<artifactId>pytorch-native-cpu</artifactId>
|
||||||
|
<classifier>${djl.platform.windows-x86_64}</classifier>
|
||||||
|
<version>2.7.1</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- linux x86 平台 (保留对应平台的配置,可以减小包大小)-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>javacpp</artifactId>
|
||||||
|
<version>${javacv.version}</version>
|
||||||
|
<classifier>${javacv.platform.linux-x86_64}</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>ffmpeg</artifactId>
|
||||||
|
<version>6.1.1-1.5.10</version>
|
||||||
|
<classifier>${javacv.platform.linux-x86_64}</classifier>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>openblas</artifactId>
|
||||||
|
<version>0.3.26-1.5.10</version>
|
||||||
|
<classifier>${javacv.platform.linux-x86_64}</classifier>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>opencv</artifactId>
|
||||||
|
<version>4.9.0-1.5.10</version>
|
||||||
|
<classifier>${javacv.platform.linux-x86_64}</classifier>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>ai.djl.pytorch</groupId>
|
||||||
|
<artifactId>pytorch-native-cpu</artifactId>
|
||||||
|
<classifier>${djl.platform.linux-x86_64}</classifier>
|
||||||
|
<version>2.7.1</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- linux aarch64 平台 (保留对应平台的配置,可以减小包大小)-->
|
<!-- linux aarch64 平台 (保留对应平台的配置,可以减小包大小)-->
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -168,16 +223,42 @@
|
|||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- <dependency>-->
|
|
||||||
<!-- <groupId>ai.djl.pytorch</groupId>-->
|
|
||||||
<!-- <artifactId>pytorch-native-cpu-precxx11</artifactId>-->
|
|
||||||
<!-- <classifier>linux-aarch64</classifier>-->
|
|
||||||
<!-- <version>2.5.1</version>-->
|
|
||||||
<!-- <scope>runtime</scope>-->
|
|
||||||
<!-- </dependency>-->
|
|
||||||
|
|
||||||
|
<!-- macOS M系列 平台 (保留对应平台的配置,可以减小包大小)-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>javacpp</artifactId>
|
||||||
|
<version>${javacv.version}</version>
|
||||||
|
<classifier>${javacv.platform.macosx-arm64}</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>ffmpeg</artifactId>
|
||||||
|
<version>6.1.1-1.5.10</version>
|
||||||
|
<classifier>${javacv.platform.macosx-arm64}</classifier>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>openblas</artifactId>
|
||||||
|
<version>0.3.26-1.5.10</version>
|
||||||
|
<classifier>${javacv.platform.macosx-arm64}</classifier>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>opencv</artifactId>
|
||||||
|
<version>4.9.0-1.5.10</version>
|
||||||
|
<classifier>${javacv.platform.macosx-arm64}</classifier>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>ai.djl.pytorch</groupId>
|
||||||
|
<artifactId>pytorch-native-cpu</artifactId>
|
||||||
|
<classifier>${djl.platform.osx-aarch64}</classifier>
|
||||||
|
<version>2.7.1</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* OCR 文本检测 示例
|
* OCR 文本检测 示例
|
||||||
* 模型下载地址:https://pan.baidu.com/s/15Noz2xHQzqMQSl1B19BobQ?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/15Noz2xHQzqMQSl1B19BobQ?pwd=1234 提取码: 1234
|
||||||
* 开发文档:http://doc.numberone.ink/
|
* 开发文档:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* OCR 行文本方向检测 示例
|
* OCR 行文本方向检测 示例
|
||||||
* 模型下载地址:https://pan.baidu.com/s/1MLfd73Vjdpnuls9-oqc9uw?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/1MLfd73Vjdpnuls9-oqc9uw?pwd=1234 提取码: 1234
|
||||||
* 开发文档:http://doc.numberone.ink/
|
* 开发文档:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -33,18 +33,12 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OCR 文本识别 示例
|
* OCR 文本识别 示例
|
||||||
* 模型下载地址:https://pan.baidu.com/s/1MLfd73Vjdpnuls9-oqc9uw?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/1MLfd73Vjdpnuls9-oqc9uw?pwd=1234 提取码: 1234
|
||||||
* 开发文档:http://doc.numberone.ink/
|
* 开发文档:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -278,78 +272,6 @@ public class OcrRecognizeDemo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 多线程文本识别
|
|
||||||
* 注意事项:
|
|
||||||
* 1、模型在外层统一创建,多个线程共享同一个模型实例,避免重复加载模型。
|
|
||||||
* 2、每个线程内部单独创建 Image 对象,避免共享图片对象带来的线程安全问题。
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void multiThreadRecognize() {
|
|
||||||
ExecutorService executorService = null;
|
|
||||||
try {
|
|
||||||
final OcrCommonRecModel recModel = getFastRecModel();
|
|
||||||
final OcrRecOptions options = new OcrRecOptions(false, true);
|
|
||||||
final String imagePath = "src/main/resources/ocr_1.jpg";
|
|
||||||
|
|
||||||
int threadCount = 5;
|
|
||||||
int taskCount = 20;
|
|
||||||
CountDownLatch countDownLatch = new CountDownLatch(taskCount);
|
|
||||||
AtomicInteger successCount = new AtomicInteger(0);
|
|
||||||
AtomicInteger failCount = new AtomicInteger(0);
|
|
||||||
List<String> errorMessages = new ArrayList<>();
|
|
||||||
executorService = Executors.newFixedThreadPool(threadCount);
|
|
||||||
|
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
for (int i = 0; i < taskCount; i++) {
|
|
||||||
final int taskIndex = i;
|
|
||||||
executorService.submit(() -> {
|
|
||||||
try {
|
|
||||||
Image image = SmartImageFactory.getInstance().fromFile(imagePath);
|
|
||||||
OcrInfo ocrInfo = recModel.recognize(image, options);
|
|
||||||
successCount.incrementAndGet();
|
|
||||||
log.info("线程:{},任务:{},识别结果:{}",
|
|
||||||
Thread.currentThread().getName(),
|
|
||||||
taskIndex,
|
|
||||||
JSONObject.toJSONString(ocrInfo));
|
|
||||||
} catch (Exception e) {
|
|
||||||
failCount.incrementAndGet();
|
|
||||||
String errorMsg = "任务" + taskIndex + "识别失败:" + e.getMessage();
|
|
||||||
synchronized (errorMessages) {
|
|
||||||
errorMessages.add(errorMsg);
|
|
||||||
}
|
|
||||||
log.error(errorMsg, e);
|
|
||||||
} finally {
|
|
||||||
countDownLatch.countDown();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
countDownLatch.await();
|
|
||||||
long costTime = System.currentTimeMillis() - startTime;
|
|
||||||
log.info("多线程识别完成,总任务数:{},成功:{},失败:{},耗时:{} ms",
|
|
||||||
taskCount, successCount.get(), failCount.get(), costTime);
|
|
||||||
|
|
||||||
if (!errorMessages.isEmpty()) {
|
|
||||||
log.error("失败详情:{}", JsonUtils.toJson(errorMessages));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
if (executorService != null) {
|
|
||||||
executorService.shutdown();
|
|
||||||
try {
|
|
||||||
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
|
|
||||||
executorService.shutdownNow();
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
executorService.shutdownNow();
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,216 +0,0 @@
|
|||||||
package smartai.examples.ocr.idcard;
|
|
||||||
|
|
||||||
import ai.djl.modality.cv.Image;
|
|
||||||
import ai.djl.util.JsonUtils;
|
|
||||||
import cn.smartjavaai.common.config.Config;
|
|
||||||
import cn.smartjavaai.common.cv.SmartImageFactory;
|
|
||||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
|
||||||
import cn.smartjavaai.common.utils.ImageUtils;
|
|
||||||
import cn.smartjavaai.ocr.config.DirectionModelConfig;
|
|
||||||
import cn.smartjavaai.ocr.config.OcrDetModelConfig;
|
|
||||||
import cn.smartjavaai.ocr.config.OcrRecModelConfig;
|
|
||||||
import cn.smartjavaai.ocr.config.OcrRecOptions;
|
|
||||||
import cn.smartjavaai.ocr.entity.IdCardBackInfo;
|
|
||||||
import cn.smartjavaai.ocr.entity.IdCardFrontInfo;
|
|
||||||
import cn.smartjavaai.ocr.entity.IdCardInfo;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrInfo;
|
|
||||||
import cn.smartjavaai.ocr.enums.CommonDetModelEnum;
|
|
||||||
import cn.smartjavaai.ocr.enums.CommonRecModelEnum;
|
|
||||||
import cn.smartjavaai.ocr.enums.DirectionModelEnum;
|
|
||||||
import cn.smartjavaai.ocr.factory.OcrModelFactory;
|
|
||||||
import cn.smartjavaai.ocr.idcard.DefaultIdCardRecognizer;
|
|
||||||
import cn.smartjavaai.ocr.idcard.IdCardPreprocessListener;
|
|
||||||
import cn.smartjavaai.ocr.model.common.detect.OcrCommonDetModel;
|
|
||||||
import cn.smartjavaai.ocr.model.common.direction.OcrDirectionModel;
|
|
||||||
import cn.smartjavaai.ocr.model.common.recognize.OcrCommonRecModel;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.junit.BeforeClass;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证识别 demo
|
|
||||||
* 使用说明:
|
|
||||||
* 1、先下载 OCR 模型
|
|
||||||
* 2、把下面的模型路径改成你自己的本地路径
|
|
||||||
* 3、把身份证图片路径改成你自己的图片路径
|
|
||||||
* 4、优先运行 recognizeFront() / recognizeBack() 查看结构化结果
|
|
||||||
*
|
|
||||||
* 模型下载地址:https://pan.baidu.com/s/1MLfd73Vjdpnuls9-oqc9uw?pwd=1234 提取码: 1234
|
|
||||||
* 开发文档:http://doc.numberone.ink/
|
|
||||||
* @author dwj
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class IdCardRecDemo {
|
|
||||||
|
|
||||||
// 设备类型
|
|
||||||
public static DeviceEnum device = DeviceEnum.CPU;
|
|
||||||
|
|
||||||
// 下载模型后,请替换成你自己的模型路径
|
|
||||||
private static final String DET_MODEL_PATH =
|
|
||||||
"/Users/wenjie/Documents/develop/model/ocr/PP-OCRv5_server_det_infer/PP-OCRv5_server_det.onnx";
|
|
||||||
private static final String REC_MODEL_PATH =
|
|
||||||
"/Users/wenjie/Documents/develop/model/ocr/PP-OCRv5_server_rec_infer/PP-OCRv5_server_rec.onnx";
|
|
||||||
private static final String DIRECTION_MODEL_PATH =
|
|
||||||
"/Users/wenjie/Documents/develop/model/ocr/PP-LCNet_x0_25_textline_ori_infer/PP-LCNet_x0_25_textline_ori_infer.onnx";
|
|
||||||
|
|
||||||
// 这里改成你自己的身份证图片路径
|
|
||||||
private static final String FRONT_IMAGE_PATH = "src/main/resources/idcard/idcard_front1.png";
|
|
||||||
private static final String BACK_IMAGE_PATH = "src/main/resources/idcard/idcard_back1.png";
|
|
||||||
|
|
||||||
@BeforeClass
|
|
||||||
public static void beforeAll() throws IOException {
|
|
||||||
// 修改缓存路径
|
|
||||||
// Config.setCachePath("/Users/xxx/smartjavaai_cache");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取文本检测模型
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public OcrCommonDetModel getDetectionModel() {
|
|
||||||
OcrDetModelConfig config = new OcrDetModelConfig();
|
|
||||||
// 文本检测模型,切换模型需要同时修改 modelEnum 及 modelPath
|
|
||||||
config.setModelEnum(CommonDetModelEnum.PP_OCR_V4_SERVER_DET_MODEL);
|
|
||||||
// 下载模型并替换本地路径
|
|
||||||
config.setDetModelPath(DET_MODEL_PATH);
|
|
||||||
config.setDevice(device);
|
|
||||||
return OcrModelFactory.getInstance().getDetModel(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取方向检测模型
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public OcrDirectionModel getDirectionModel() {
|
|
||||||
DirectionModelConfig directionModelConfig = new DirectionModelConfig();
|
|
||||||
// 行文本方向检测模型,切换模型需要同时修改 modelEnum 及 modelPath
|
|
||||||
directionModelConfig.setModelEnum(DirectionModelEnum.PP_LCNET_X0_25);
|
|
||||||
// 下载模型并替换本地路径
|
|
||||||
directionModelConfig.setModelPath(DIRECTION_MODEL_PATH);
|
|
||||||
directionModelConfig.setTextDetModel(getDetectionModel());
|
|
||||||
directionModelConfig.setDevice(device);
|
|
||||||
return OcrModelFactory.getInstance().getDirectionModel(directionModelConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 OCR 识别模型
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public OcrCommonRecModel getRecModel() {
|
|
||||||
OcrRecModelConfig recModelConfig = new OcrRecModelConfig();
|
|
||||||
// 文本识别模型,切换模型需要同时修改 modelEnum 及 modelPath
|
|
||||||
recModelConfig.setRecModelEnum(CommonRecModelEnum.PP_OCR_V5_MOBILE_REC_MODEL);
|
|
||||||
// 下载模型并替换本地路径
|
|
||||||
recModelConfig.setRecModelPath(REC_MODEL_PATH);
|
|
||||||
recModelConfig.setTextDetModel(getDetectionModel());
|
|
||||||
recModelConfig.setDevice(device);
|
|
||||||
return OcrModelFactory.getInstance().getRecModel(recModelConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建身份证识别器
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public DefaultIdCardRecognizer getIdCardRecognizer() {
|
|
||||||
return new DefaultIdCardRecognizer()
|
|
||||||
.setRecModel(getRecModel())
|
|
||||||
// 身份证识别建议按行返回,方向矫正交给预处理逻辑处理
|
|
||||||
.setRecOptions(new OcrRecOptions(false, true))
|
|
||||||
.setDirectionModel(getDirectionModel())
|
|
||||||
.setEnablePreprocess(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证正面识别
|
|
||||||
* 可识别:姓名、性别、民族、出生日期、住址、身份证号
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void recognizeFront() {
|
|
||||||
try {
|
|
||||||
DefaultIdCardRecognizer recognizer = getIdCardRecognizer();
|
|
||||||
Image image = SmartImageFactory.getInstance().fromFile(FRONT_IMAGE_PATH);
|
|
||||||
IdCardFrontInfo result = recognizer.recognizeFront(image);
|
|
||||||
log.info("身份证正面识别结果:{}", JsonUtils.toJson(result));
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("身份证正面识别失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证反面识别
|
|
||||||
* 可识别:签发机关、有效期开始时间、有效期结束时间
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void recognizeBack() {
|
|
||||||
try {
|
|
||||||
DefaultIdCardRecognizer recognizer = getIdCardRecognizer();
|
|
||||||
Image image = SmartImageFactory.getInstance().fromFile(BACK_IMAGE_PATH);
|
|
||||||
IdCardBackInfo result = recognizer.recognizeBack(image);
|
|
||||||
log.info("身份证反面识别结果:{}", JsonUtils.toJson(result));
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("身份证反面识别失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同时识别身份证正反面
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void recognizeBoth() {
|
|
||||||
try {
|
|
||||||
DefaultIdCardRecognizer recognizer = getIdCardRecognizer();
|
|
||||||
Image frontImage = SmartImageFactory.getInstance().fromFile(FRONT_IMAGE_PATH);
|
|
||||||
Image backImage = SmartImageFactory.getInstance().fromFile(BACK_IMAGE_PATH);
|
|
||||||
IdCardInfo result = recognizer.recognizeBoth(frontImage, backImage);
|
|
||||||
log.info("身份证正反面识别结果:{}", JsonUtils.toJson(result));
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("身份证正反面识别失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证识别并输出调试图片
|
|
||||||
* 适合排查“为什么没识别出来”,不适合生产环境使用
|
|
||||||
*/
|
|
||||||
@Test
|
|
||||||
public void recognizeFrontWithDebugImages() {
|
|
||||||
try {
|
|
||||||
Path debugDir = Paths.get("output/idcard_debug");
|
|
||||||
Files.createDirectories(debugDir);
|
|
||||||
String filePrefix = "idcard_front";
|
|
||||||
DefaultIdCardRecognizer recognizer = getIdCardRecognizer()
|
|
||||||
// .setPreprocessListener(createDebugListener(debugDir, "idcard_front"));
|
|
||||||
.setPreprocessListener(new IdCardPreprocessListener() {
|
|
||||||
@Override
|
|
||||||
public void onAfterDirection(Image image) {
|
|
||||||
saveDebugImage(image, debugDir.resolve(filePrefix + "_step1_direction.png"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAfterRecognize(Image image, OcrInfo ocrInfo) {
|
|
||||||
saveDebugImage(image, debugDir.resolve(filePrefix + "_step2_ocr_boxes.png"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Image image = SmartImageFactory.getInstance().fromFile(FRONT_IMAGE_PATH);
|
|
||||||
IdCardFrontInfo result = recognizer.recognizeFront(image);
|
|
||||||
log.info("身份证正面识别结果:{}", JsonUtils.toJson(result));
|
|
||||||
log.info("调试图片已输出到:{}", debugDir.toAbsolutePath());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("身份证调试识别失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveDebugImage(Image image, Path outputPath) {
|
|
||||||
try {
|
|
||||||
ImageUtils.save(image, outputPath, "png");
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,7 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* 车牌识别demo
|
* 车牌识别demo
|
||||||
* 模型下载地址:https://pan.baidu.com/s/1YEP56UqYcL-Op80M6JAreA?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/1YEP56UqYcL-Op80M6JAreA?pwd=1234 提取码: 1234
|
||||||
* 开发文档:http://doc.numberone.ink/
|
* 开发文档:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* OCR 表格识别 示例
|
* OCR 表格识别 示例
|
||||||
* 模型下载地址:https://pan.baidu.com/s/1MLfd73Vjdpnuls9-oqc9uw?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/1MLfd73Vjdpnuls9-oqc9uw?pwd=1234 提取码: 1234
|
||||||
* 开发文档:http://doc.numberone.ink/
|
* 开发文档:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 997 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 418 KiB |
@@ -33,6 +33,6 @@ src
|
|||||||
## 📄 文档
|
## 📄 文档
|
||||||
|
|
||||||
有关完整使用说明,请查阅 SmartJavaAI 官方文档:
|
有关完整使用说明,请查阅 SmartJavaAI 官方文档:
|
||||||
[http://doc.numberone.ink](http://doc.numberone.ink)
|
[http://doc.smartjavaai.cn](http://doc.smartjavaai.cn)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<maven.compiler.source>11</maven.compiler.source>
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
<maven.compiler.target>11</maven.compiler.target>
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<smartjavaai.version>1.1.2</smartjavaai.version>
|
<smartjavaai.version>1.1.1</smartjavaai.version>
|
||||||
<!--如果打包运行,需要替换成你的main-->
|
<!--如果打包运行,需要替换成你的main-->
|
||||||
<exec.mainClass>smartai.examples.speech.asr.common.OcrRecognizeDemo</exec.mainClass>
|
<exec.mainClass>smartai.examples.speech.asr.common.OcrRecognizeDemo</exec.mainClass>
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import java.nio.file.Paths;
|
|||||||
/**
|
/**
|
||||||
* 语音识别demo(Vosk、Whisper)
|
* 语音识别demo(Vosk、Whisper)
|
||||||
* 模型下载网盘:https://pan.baidu.com/s/1kiMF5MF641R7LTn1GpB2lQ?pwd=1234 提取码: 1234
|
* 模型下载网盘:https://pan.baidu.com/s/1kiMF5MF641R7LTn1GpB2lQ?pwd=1234 提取码: 1234
|
||||||
* 文档地址:http://doc.numberone.ink/
|
* 文档地址:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -40,6 +40,6 @@
|
|||||||
## 📄 文档
|
## 📄 文档
|
||||||
|
|
||||||
有关完整使用说明,请查阅 SmartJavaAI 官方文档:
|
有关完整使用说明,请查阅 SmartJavaAI 官方文档:
|
||||||
[http://doc.numberone.ink](http://doc.numberone.ink)
|
[http://doc.smartjavaai.cn](http://doc.smartjavaai.cn)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<maven.compiler.source>11</maven.compiler.source>
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
<maven.compiler.target>11</maven.compiler.target>
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<smartjavaai.version>1.1.2</smartjavaai.version>
|
<smartjavaai.version>1.1.1</smartjavaai.version>
|
||||||
<!--如果打包运行,需要替换成你的main-->
|
<!--如果打包运行,需要替换成你的main-->
|
||||||
<exec.mainClass>smartai.examples.nlp.translation.TranslationDemo</exec.mainClass>
|
<exec.mainClass>smartai.examples.nlp.translation.TranslationDemo</exec.mainClass>
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import java.io.IOException;
|
|||||||
/**
|
/**
|
||||||
* 机器翻译Demo
|
* 机器翻译Demo
|
||||||
* 模型下载地址:https://pan.baidu.com/s/1wf7btnb4cyBFv7DB7baHnw?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/1wf7btnb4cyBFv7DB7baHnw?pwd=1234 提取码: 1234
|
||||||
* 开发文档:http://doc.numberone.ink/
|
* 开发文档:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -96,6 +96,6 @@ vision-example/
|
|||||||
## 📄 文档
|
## 📄 文档
|
||||||
|
|
||||||
有关完整使用说明,请查阅 SmartJavaAI 官方文档:
|
有关完整使用说明,请查阅 SmartJavaAI 官方文档:
|
||||||
[http://doc.numberone.ink](http://doc.numberone.ink)
|
[http://doc.smartjavaai.cn](http://doc.smartjavaai.cn)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<maven.compiler.source>11</maven.compiler.source>
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
<maven.compiler.target>11</maven.compiler.target>
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<smartjavaai.version>1.1.2</smartjavaai.version>
|
<smartjavaai.version>1.1.1</smartjavaai.version>
|
||||||
<!--如果打包运行,需要替换成你的main-->
|
<!--如果打包运行,需要替换成你的main-->
|
||||||
<exec.mainClass>smartai.examples.vision.ObjectDetectionDemo</exec.mainClass>
|
<exec.mainClass>smartai.examples.vision.ObjectDetectionDemo</exec.mainClass>
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import java.util.Arrays;
|
|||||||
/**
|
/**
|
||||||
* 动作识别Demo
|
* 动作识别Demo
|
||||||
* 模型下载地址:https://pan.baidu.com/s/17doY4pgZM9EbtSIaoCWWCA?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/17doY4pgZM9EbtSIaoCWWCA?pwd=1234 提取码: 1234
|
||||||
* 文档地址:http://doc.numberone.ink/
|
* 文档地址:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -41,7 +41,7 @@ public class ActionRecognizeDemo {
|
|||||||
/**
|
/**
|
||||||
* 获取动作识别模型
|
* 获取动作识别模型
|
||||||
* 注意事项:
|
* 注意事项:
|
||||||
* 1、不同模型支持的动作类别不同,请查看文档:http://doc.numberone.ink
|
* 1、不同模型支持的动作类别不同,请查看文档:http://doc.smartjavaai.cn
|
||||||
*/
|
*/
|
||||||
public ActionRecModel getModel(){
|
public ActionRecModel getModel(){
|
||||||
ActionRecModelConfig config = new ActionRecModelConfig();
|
ActionRecModelConfig config = new ActionRecModelConfig();
|
||||||
@@ -62,7 +62,7 @@ public class ActionRecognizeDemo {
|
|||||||
/**
|
/**
|
||||||
* 动作识别
|
* 动作识别
|
||||||
* 注意事项:
|
* 注意事项:
|
||||||
* 1、不同模型支持的动作类别不同,请查看文档:http://doc.numberone.ink
|
* 1、不同模型支持的动作类别不同,请查看文档:http://doc.smartjavaai.cn
|
||||||
* 2、图片中应该只包含单一动作人物
|
* 2、图片中应该只包含单一动作人物
|
||||||
* 3、动作识别,只做图片分类,并不做人物定位
|
* 3、动作识别,只做图片分类,并不做人物定位
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import java.nio.file.Paths;
|
|||||||
/**
|
/**
|
||||||
* 实例分割 Demo
|
* 实例分割 Demo
|
||||||
* 模型下载地址:https://pan.baidu.com/s/12nRRY9JFNDwLeg63jfBerA?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/12nRRY9JFNDwLeg63jfBerA?pwd=1234 提取码: 1234
|
||||||
* 文档地址:http://doc.numberone.ink/
|
* 文档地址:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -50,7 +50,7 @@ public class InstanceSegDemo {
|
|||||||
/**
|
/**
|
||||||
* 获取实例分割模型
|
* 获取实例分割模型
|
||||||
* 注意事项:
|
* 注意事项:
|
||||||
* 1、更多模型请查看文档:http://doc.numberone.ink
|
* 1、更多模型请查看文档:http://doc.smartjavaai.cn
|
||||||
*/
|
*/
|
||||||
public InstanceSegModel getModel(){
|
public InstanceSegModel getModel(){
|
||||||
InstanceSegModelConfig config = new InstanceSegModelConfig();
|
InstanceSegModelConfig config = new InstanceSegModelConfig();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import java.util.Arrays;
|
|||||||
/**
|
/**
|
||||||
* obb旋转框检测demo
|
* obb旋转框检测demo
|
||||||
* 模型下载地址:https://pan.baidu.com/s/1-tC0u-aha3tnMQwy8FKy1Q?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/1-tC0u-aha3tnMQwy8FKy1Q?pwd=1234 提取码: 1234
|
||||||
* 文档地址:http://doc.numberone.ink/
|
* 文档地址:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -45,7 +45,7 @@ public class ObbDetDemo {
|
|||||||
/**
|
/**
|
||||||
* 获取旋转框检测模型
|
* 获取旋转框检测模型
|
||||||
* 注意事项:
|
* 注意事项:
|
||||||
* 1、更多模型请查看文档:http://doc.numberone.ink
|
* 1、更多模型请查看文档:http://doc.smartjavaai.cn
|
||||||
* 2、模型可检测物体请查看:模型同目录文件synset.txt
|
* 2、模型可检测物体请查看:模型同目录文件synset.txt
|
||||||
*/
|
*/
|
||||||
public ObbDetModel getModel(){
|
public ObbDetModel getModel(){
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import java.util.concurrent.CountDownLatch;
|
|||||||
/**
|
/**
|
||||||
* 目标检测模型demo
|
* 目标检测模型demo
|
||||||
* 模型下载地址:https://pan.baidu.com/s/10aTOLBlR6EG-sq6g0OkAWg?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/10aTOLBlR6EG-sq6g0OkAWg?pwd=1234 提取码: 1234
|
||||||
* 文档地址:http://doc.numberone.ink/
|
* 文档地址:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -66,7 +66,7 @@ public class ObjectDetectionDemo {
|
|||||||
/**
|
/**
|
||||||
* 获取目标检测模型
|
* 获取目标检测模型
|
||||||
* 注意事项:
|
* 注意事项:
|
||||||
* 1、更多模型请查看文档:http://doc.numberone.ink/objectdetect.html
|
* 1、更多模型请查看文档:http://doc.smartjavaai.cn/objectdetect.html
|
||||||
*/
|
*/
|
||||||
public DetectorModel getModel(){
|
public DetectorModel getModel(){
|
||||||
DetectorModelConfig config = new DetectorModelConfig();
|
DetectorModelConfig config = new DetectorModelConfig();
|
||||||
@@ -147,7 +147,7 @@ public class ObjectDetectionDemo {
|
|||||||
DetectorModelConfig config = new DetectorModelConfig();
|
DetectorModelConfig config = new DetectorModelConfig();
|
||||||
//目标检测模型,切换模型需要同时修改modelEnum及modelPath
|
//目标检测模型,切换模型需要同时修改modelEnum及modelPath
|
||||||
config.setModelEnum(DetectorModelEnum.YOLOV12_CUSTOM_ONNX);
|
config.setModelEnum(DetectorModelEnum.YOLOV12_CUSTOM_ONNX);
|
||||||
//模型所在路径,synset.txt也需要放在同目录下(分类文件,具体请看文档:http://doc.numberone.ink/objectdetect.html#%E4%BD%BF%E7%94%A8%E8%87%AA%E5%B7%B1%E8%AE%AD%E7%BB%83%E7%9A%84%E6%A8%A1%E5%9E%8B%E6%A3%80%E6%B5%8B)
|
//模型所在路径,synset.txt也需要放在同目录下(分类文件,具体请看文档:http://doc.smartjavaai.cn/objectdetect.html#%E4%BD%BF%E7%94%A8%E8%87%AA%E5%B7%B1%E8%AE%AD%E7%BB%83%E7%9A%84%E6%A8%A1%E5%9E%8B%E6%A3%80%E6%B5%8B)
|
||||||
config.setModelPath("/Users/xxx/Documents/develop/fire_model/best.onnx");
|
config.setModelPath("/Users/xxx/Documents/develop/fire_model/best.onnx");
|
||||||
//模型训练时图片宽度
|
//模型训练时图片宽度
|
||||||
config.putCustomParam("width", 640);//resize 宽
|
config.putCustomParam("width", 640);//resize 宽
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import java.nio.file.Paths;
|
|||||||
/**
|
/**
|
||||||
* 行人检测案例
|
* 行人检测案例
|
||||||
* 模型下载地址:https://pan.baidu.com/s/1EWfExw7pYjKEH5uR5wf3Rw?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/1EWfExw7pYjKEH5uR5wf3Rw?pwd=1234 提取码: 1234
|
||||||
* 文档地址:http://doc.numberone.ink/
|
* 文档地址:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import java.nio.file.Paths;
|
|||||||
/**
|
/**
|
||||||
* 姿态估计demo
|
* 姿态估计demo
|
||||||
* 模型下载地址:https://pan.baidu.com/s/1pPYyl1V2CpcMYCO8CJQHGg?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/1pPYyl1V2CpcMYCO8CJQHGg?pwd=1234 提取码: 1234
|
||||||
* 文档地址:http://doc.numberone.ink/
|
* 文档地址:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -42,7 +42,7 @@ public class PoseDetDemo {
|
|||||||
/**
|
/**
|
||||||
* 获取姿态估计模型
|
* 获取姿态估计模型
|
||||||
* 注意事项:
|
* 注意事项:
|
||||||
* 1、更多模型请查看文档:http://doc.numberone.ink
|
* 1、更多模型请查看文档:http://doc.smartjavaai.cn
|
||||||
*/
|
*/
|
||||||
public PoseModel getModel(){
|
public PoseModel getModel(){
|
||||||
PoseModelConfig config = new PoseModelConfig();
|
PoseModelConfig config = new PoseModelConfig();
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import java.util.Arrays;
|
|||||||
/**
|
/**
|
||||||
* 语义分割 Demo 通过网盘分享的文件:语义分割(semantic_segmentation)
|
* 语义分割 Demo 通过网盘分享的文件:语义分割(semantic_segmentation)
|
||||||
* 模型下载地址:https://pan.baidu.com/s/18gs9E5h_d9imPmNLHuDo9A?pwd=1234 提取码: 1234
|
* 模型下载地址:https://pan.baidu.com/s/18gs9E5h_d9imPmNLHuDo9A?pwd=1234 提取码: 1234
|
||||||
* 文档地址:http://doc.numberone.ink/
|
* 文档地址:http://doc.smartjavaai.cn/
|
||||||
* @author dwj
|
* @author dwj
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -45,7 +45,7 @@ public class SemSegDemo {
|
|||||||
/**
|
/**
|
||||||
* 获取语义分割模型
|
* 获取语义分割模型
|
||||||
* 注意事项:
|
* 注意事项:
|
||||||
* 1、更多模型请查看文档:http://doc.numberone.ink
|
* 1、更多模型请查看文档:http://doc.smartjavaai.cn
|
||||||
*/
|
*/
|
||||||
public SemSegModel getModel(){
|
public SemSegModel getModel(){
|
||||||
SemSegModelConfig config = new SemSegModelConfig();
|
SemSegModelConfig config = new SemSegModelConfig();
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-parent</artifactId>
|
<artifactId>smartjavaai-parent</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>face</artifactId>
|
<artifactId>face</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
<name>face</name>
|
<name>face</name>
|
||||||
<description>SmartJavaAI</description>
|
<description>SmartJavaAI</description>
|
||||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-parent</artifactId>
|
<artifactId>smartjavaai-parent</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>ocr</artifactId>
|
<artifactId>ocr</artifactId>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
<name>ocr</name>
|
<name>ocr</name>
|
||||||
<description>SmartJavaAI</description>
|
<description>SmartJavaAI</description>
|
||||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ public class OcrRecOptions {
|
|||||||
private boolean enableLineSplit = true;
|
private boolean enableLineSplit = true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public OcrRecOptions(boolean enableDirectionCorrect, boolean enableLineSplit) {
|
public OcrRecOptions(boolean enableDirectionCorrect, boolean enableLineSplit) {
|
||||||
this.enableDirectionCorrect = enableDirectionCorrect;
|
this.enableDirectionCorrect = enableDirectionCorrect;
|
||||||
this.enableLineSplit = enableLineSplit;
|
this.enableLineSplit = enableLineSplit;
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package cn.smartjavaai.ocr.entity;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证反面信息
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class IdCardBackInfo {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 签发机关
|
|
||||||
*/
|
|
||||||
private String issuingAuthority;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 有效期开始日期,格式:yyyy-MM-dd
|
|
||||||
*/
|
|
||||||
private String validFrom;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 有效期结束日期,格式:yyyy-MM-dd 或 "长期"
|
|
||||||
*/
|
|
||||||
private String validTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package cn.smartjavaai.ocr.entity;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证正面信息
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class IdCardFrontInfo {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 姓名
|
|
||||||
*/
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 性别(男/女)
|
|
||||||
*/
|
|
||||||
private String gender;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 民族(如:汉、满、回等,不带“族”字)
|
|
||||||
*/
|
|
||||||
private String ethnicity;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 公民身份号码
|
|
||||||
*/
|
|
||||||
private String idNumber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 出生日期,格式:yyyy-MM-dd
|
|
||||||
*/
|
|
||||||
private String birthday;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 住址
|
|
||||||
*/
|
|
||||||
private String address;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,26 +1,12 @@
|
|||||||
package cn.smartjavaai.ocr.entity;
|
package cn.smartjavaai.ocr.entity;
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 身份证信息(聚合正反面)
|
* 身份证信息
|
||||||
*
|
|
||||||
* 当前主要使用正面信息,预留反面字段,方便后续扩展。
|
|
||||||
*
|
|
||||||
* @author dwj
|
* @author dwj
|
||||||
* @date 2025/5/22
|
* @date 2025/5/22
|
||||||
*/
|
*/
|
||||||
@Data
|
|
||||||
public class IdCardInfo {
|
public class IdCardInfo {
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证正面信息
|
|
||||||
*/
|
|
||||||
private IdCardFrontInfo front;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证反面信息
|
|
||||||
*/
|
|
||||||
private IdCardBackInfo back;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
package cn.smartjavaai.ocr.idcard;
|
|
||||||
|
|
||||||
import ai.djl.modality.cv.Image;
|
|
||||||
import ai.djl.util.JsonUtils;
|
|
||||||
import cn.smartjavaai.common.utils.ImageUtils;
|
|
||||||
import cn.smartjavaai.ocr.config.OcrRecOptions;
|
|
||||||
import cn.smartjavaai.ocr.entity.IdCardBackInfo;
|
|
||||||
import cn.smartjavaai.ocr.entity.IdCardFrontInfo;
|
|
||||||
import cn.smartjavaai.ocr.entity.IdCardInfo;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrItem;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrInfo;
|
|
||||||
import cn.smartjavaai.ocr.model.common.direction.OcrDirectionModel;
|
|
||||||
import cn.smartjavaai.ocr.model.common.recognize.OcrCommonRecModel;
|
|
||||||
import cn.smartjavaai.ocr.utils.OcrUtils;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import lombok.experimental.Accessors;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证 OCR 识别服务实现
|
|
||||||
*
|
|
||||||
* - 支持从 Image 开始:内部完成预处理(方向矫正 OcrDirectionModel)+ OCR + 解析
|
|
||||||
* - 支持从 OcrInfo 开始:只做结构化解析
|
|
||||||
*
|
|
||||||
* 通过 Lombok @Accessors(chain = true) 支持链式设置依赖。
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Data
|
|
||||||
@Accessors(chain = true)
|
|
||||||
public class DefaultIdCardRecognizer implements IdCardRecognizer {
|
|
||||||
|
|
||||||
private static final int DEBUG_DRAW_FONT_SIZE = 20;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文本识别模型(可选;仅当从 Image 开始时需要)
|
|
||||||
*/
|
|
||||||
private OcrCommonRecModel recModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 识别选项(如是否方向矫正、是否按行返回等)
|
|
||||||
*/
|
|
||||||
private OcrRecOptions recOptions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 方向检测模型(可选;预处理时用于 0/90/180/270° 整图方向矫正)。
|
|
||||||
* 未设置则跳过方向矫正。
|
|
||||||
*/
|
|
||||||
private OcrDirectionModel directionModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 图像预处理器(方向矫正,可选,默认按需 lazy-init)
|
|
||||||
*/
|
|
||||||
private IdCardPreprocessor preprocessor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 正面解析器(默认按需 lazy-init 为 IdCardFrontParser)
|
|
||||||
*/
|
|
||||||
private IdCardParser<IdCardFrontInfo> frontParser;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 反面解析器(默认按需 lazy-init 为 IdCardBackParser)
|
|
||||||
*/
|
|
||||||
private IdCardParser<IdCardBackInfo> backParser;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验器(可选,默认按需 lazy-init 为 IdCardValidator)
|
|
||||||
*/
|
|
||||||
private IdCardValidator validator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否进行图像预处理(方向矫正)。默认 true。
|
|
||||||
* 若图片已校正,可设为 false 以跳过预处理、节省时间。
|
|
||||||
*/
|
|
||||||
private boolean enablePreprocess = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 预处理调试监听器(可选)。
|
|
||||||
* 若设置,则在方向矫正阶段结束后回调当前图像,
|
|
||||||
* 方便最终用户在需要时保存中间结果进行排查和可视化调试。
|
|
||||||
* 默认 null,不影响正常业务逻辑。
|
|
||||||
*/
|
|
||||||
private IdCardPreprocessListener preprocessListener;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IdCardFrontInfo recognizeFront(Image image) {
|
|
||||||
if (recModel == null) {
|
|
||||||
throw new IllegalStateException("recModel 未配置,无法从 Image 执行身份证识别,请先设置 recModel 或改用 recognizeFront(OcrInfo)。");
|
|
||||||
}
|
|
||||||
long totalStart = System.nanoTime();
|
|
||||||
Image toRecognize = image;
|
|
||||||
IdCardPreprocessResult preprocessResult = null;
|
|
||||||
if (enablePreprocess) {
|
|
||||||
long preprocessStart = System.nanoTime();
|
|
||||||
IdCardPreprocessor pp = (preprocessor != null) ? preprocessor : (preprocessor = new IdCardPreprocessor());
|
|
||||||
preprocessResult = pp.preprocess(image, recModel.getTextDetModel(), directionModel, preprocessListener);
|
|
||||||
toRecognize = preprocessResult.getProcessedImage();
|
|
||||||
log.debug("身份证正面预处理耗时={}ms", elapsedMillis(preprocessStart));
|
|
||||||
}
|
|
||||||
long recognizeStart = System.nanoTime();
|
|
||||||
OcrInfo ocrInfo = preprocessResult != null
|
|
||||||
&& !preprocessResult.isRotated()
|
|
||||||
&& preprocessResult.getReusableBoxes() != null
|
|
||||||
&& !preprocessResult.getReusableBoxes().isEmpty()
|
|
||||||
? recModel.recognize(toRecognize, preprocessResult.getReusableBoxes(), recOptions)
|
|
||||||
: recModel.recognize(toRecognize, recOptions);
|
|
||||||
log.debug("身份证正面OCR模型调用耗时={}ms", elapsedMillis(recognizeStart));
|
|
||||||
notifyAfterRecognize(toRecognize, ocrInfo);
|
|
||||||
IdCardFrontInfo info = recognizeFront(ocrInfo);
|
|
||||||
log.debug("身份证正面总耗时={}ms", elapsedMillis(totalStart));
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IdCardBackInfo recognizeBack(Image image) {
|
|
||||||
if (recModel == null) {
|
|
||||||
throw new IllegalStateException("recModel 未配置,无法从 Image 执行身份证识别,请先设置 recModel 或改用 recognizeBack(OcrInfo)。");
|
|
||||||
}
|
|
||||||
long totalStart = System.nanoTime();
|
|
||||||
Image toRecognize = image;
|
|
||||||
IdCardPreprocessResult preprocessResult = null;
|
|
||||||
if (enablePreprocess) {
|
|
||||||
long preprocessStart = System.nanoTime();
|
|
||||||
IdCardPreprocessor pp = (preprocessor != null) ? preprocessor : (preprocessor = new IdCardPreprocessor());
|
|
||||||
preprocessResult = pp.preprocess(image, recModel.getTextDetModel(), directionModel, preprocessListener);
|
|
||||||
toRecognize = preprocessResult.getProcessedImage();
|
|
||||||
log.debug("身份证反面预处理耗时={}ms", elapsedMillis(preprocessStart));
|
|
||||||
}
|
|
||||||
long recognizeStart = System.nanoTime();
|
|
||||||
OcrInfo ocrInfo = preprocessResult != null
|
|
||||||
&& !preprocessResult.isRotated()
|
|
||||||
&& preprocessResult.getReusableBoxes() != null
|
|
||||||
&& !preprocessResult.getReusableBoxes().isEmpty()
|
|
||||||
? recModel.recognize(toRecognize, preprocessResult.getReusableBoxes(), recOptions)
|
|
||||||
: recModel.recognize(toRecognize, recOptions);
|
|
||||||
log.debug("身份证反面OCR模型调用耗时={}ms", elapsedMillis(recognizeStart));
|
|
||||||
notifyAfterRecognize(toRecognize, ocrInfo);
|
|
||||||
IdCardBackInfo info = recognizeBack(ocrInfo);
|
|
||||||
log.debug("身份证反面总耗时={}ms", elapsedMillis(totalStart));
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyAfterRecognize(Image toRecognize, OcrInfo ocrInfo) {
|
|
||||||
if (preprocessListener == null || toRecognize == null || ocrInfo == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
//打印
|
|
||||||
log.debug("身份证 OCR 调试结果:{}", JsonUtils.toJson(ocrInfo));
|
|
||||||
Image drawImage = ImageUtils.copy(toRecognize);
|
|
||||||
List<OcrItem> ocrItems = ocrInfo.getOcrItemList();
|
|
||||||
if ((ocrItems == null || ocrItems.isEmpty()) && ocrInfo.getLineList() != null) {
|
|
||||||
ocrItems = ocrInfo.flattenLines();
|
|
||||||
}
|
|
||||||
if (ocrItems != null && !ocrItems.isEmpty()) {
|
|
||||||
OcrUtils.drawOcrResult(drawImage, ocrItems, DEBUG_DRAW_FONT_SIZE);
|
|
||||||
}
|
|
||||||
preprocessListener.onAfterRecognize(drawImage, ocrInfo);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("身份证 OCR 调试结果绘制失败,跳过 onAfterRecognize 回调。", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IdCardFrontInfo recognizeFront(OcrInfo ocrInfo) {
|
|
||||||
long start = System.nanoTime();
|
|
||||||
IdCardParser<IdCardFrontInfo> parser =
|
|
||||||
(frontParser != null) ? frontParser : (frontParser = new IdCardFrontParser());
|
|
||||||
IdCardFrontInfo info = parser.parse(ocrInfo);
|
|
||||||
IdCardValidator v = (validator != null) ? validator : (validator = new IdCardValidator());
|
|
||||||
v.validateFront(info);
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IdCardBackInfo recognizeBack(OcrInfo ocrInfo) {
|
|
||||||
IdCardParser<IdCardBackInfo> parser =
|
|
||||||
(backParser != null) ? backParser : (backParser = new IdCardBackParser());
|
|
||||||
IdCardBackInfo info = parser.parse(ocrInfo);
|
|
||||||
IdCardValidator v = (validator != null) ? validator : (validator = new IdCardValidator());
|
|
||||||
v.validateBack(info);
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IdCardInfo recognizeBoth(Image frontImage, Image backImage) {
|
|
||||||
long totalStart = System.nanoTime();
|
|
||||||
IdCardInfo info = new IdCardInfo();
|
|
||||||
info.setFront(recognizeFront(frontImage));
|
|
||||||
info.setBack(recognizeBack(backImage));
|
|
||||||
IdCardValidator v = (validator != null) ? validator : (validator = new IdCardValidator());
|
|
||||||
v.validate(info);
|
|
||||||
log.debug("身份证正反面识别总耗时={}ms", elapsedMillis(totalStart));
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
private long elapsedMillis(long startNanos) {
|
|
||||||
return (System.nanoTime() - startNanos) / 1_000_000;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
package cn.smartjavaai.ocr.idcard;
|
|
||||||
|
|
||||||
import cn.smartjavaai.ocr.entity.IdCardBackInfo;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrBox;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrInfo;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrItem;
|
|
||||||
import cn.smartjavaai.ocr.utils.BoxUtils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.IdentityHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证反面解析逻辑。
|
|
||||||
*
|
|
||||||
* 解析:
|
|
||||||
* - 签发机关
|
|
||||||
* - 有效期限(起止日期 / 长期)
|
|
||||||
*/
|
|
||||||
public class IdCardBackParser implements IdCardParser<IdCardBackInfo> {
|
|
||||||
|
|
||||||
private static final Pattern VALID_PERIOD_PATTERN = Pattern.compile(
|
|
||||||
"((?:19|20)\\d{2})[./-]?((?:1[0-2])|(?:0[1-9]))[./-]?((?:3[01])|(?:[12]\\d)|(?:0[1-9]))" +
|
|
||||||
"\\s*[-一至到~]\\s*" +
|
|
||||||
"(((?:19|20)\\d{2})[./-]?((?:1[0-2])|(?:0[1-9]))[./-]?((?:3[01])|(?:[12]\\d)|(?:0[1-9]))|长期)"
|
|
||||||
);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IdCardBackInfo parse(OcrInfo ocrInfo) {
|
|
||||||
IdCardBackInfo info = new IdCardBackInfo();
|
|
||||||
if (ocrInfo == null || ocrInfo.getLineList() == null || ocrInfo.getLineList().isEmpty()) {
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<OcrItem> allItems = ocrInfo.getLineList().stream()
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.flatMap(List::stream)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
allItems = filterSmallBoxes(allItems);
|
|
||||||
|
|
||||||
if (allItems.isEmpty()) {
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
Map<OcrBox, OcrItem> itemByBox = buildBoxItemMap(allItems);
|
|
||||||
|
|
||||||
OcrItem authorityLabel = findLabel(allItems, Arrays.asList("签发机关", "签发機关", "签发机関", "签发"));
|
|
||||||
if (authorityLabel != null) {
|
|
||||||
String authority = findIssuingAuthority(authorityLabel, allItems, itemByBox);
|
|
||||||
info.setIssuingAuthority(authority);
|
|
||||||
}
|
|
||||||
|
|
||||||
OcrItem validLabel = findLabel(allItems, Arrays.asList("有效期限", "有效期", "有效期限:", "有效期限:"));
|
|
||||||
if (validLabel != null) {
|
|
||||||
ValidPeriod validPeriod = findValidPeriod(validLabel, allItems, itemByBox);
|
|
||||||
if (validPeriod != null) {
|
|
||||||
info.setValidFrom(validPeriod.validFrom);
|
|
||||||
info.setValidTo(validPeriod.validTo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.getIssuingAuthority() == null || info.getIssuingAuthority().isEmpty()) {
|
|
||||||
info.setIssuingAuthority(findIssuingAuthorityByGlobalFallback(allItems));
|
|
||||||
}
|
|
||||||
if (info.getValidFrom() == null || info.getValidTo() == null) {
|
|
||||||
ValidPeriod fallback = findValidPeriodByGlobalFallback(allItems);
|
|
||||||
if (fallback != null) {
|
|
||||||
if (info.getValidFrom() == null) {
|
|
||||||
info.setValidFrom(fallback.validFrom);
|
|
||||||
}
|
|
||||||
if (info.getValidTo() == null) {
|
|
||||||
info.setValidTo(fallback.validTo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String findIssuingAuthority(OcrItem authorityLabel, List<OcrItem> allItems, Map<OcrBox, OcrItem> itemByBox) {
|
|
||||||
String selfAuthority = extractIssuingAuthority(authorityLabel.getText());
|
|
||||||
if (selfAuthority != null) {
|
|
||||||
return selfAuthority;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<OcrBox> boxList = allItems.stream()
|
|
||||||
.map(OcrItem::getOcrBox)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
OcrBox anchorBox = authorityLabel.getOcrBox();
|
|
||||||
String authority = findAuthorityByDirection(anchorBox, itemByBox, boxList, BoxUtils.Direction.RIGHT, 3);
|
|
||||||
if (authority != null) {
|
|
||||||
return authority;
|
|
||||||
}
|
|
||||||
authority = findAuthorityByDirection(anchorBox, itemByBox, boxList, BoxUtils.Direction.DOWN, 3);
|
|
||||||
if (authority != null) {
|
|
||||||
return authority;
|
|
||||||
}
|
|
||||||
authority = findAuthorityByDirection(anchorBox, itemByBox, boxList, BoxUtils.Direction.UP, 2);
|
|
||||||
if (authority != null) {
|
|
||||||
return authority;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String findAuthorityByDirection(OcrBox anchorBox,
|
|
||||||
Map<OcrBox, OcrItem> itemByBox,
|
|
||||||
List<OcrBox> boxList,
|
|
||||||
BoxUtils.Direction direction,
|
|
||||||
int limit) {
|
|
||||||
List<OcrBox> neighbors = BoxUtils.findNearestBoxes(anchorBox, boxList, direction, limit);
|
|
||||||
if (neighbors == null || neighbors.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<OcrItem> neighborItems = neighbors.stream()
|
|
||||||
.map(itemByBox::get)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
for (OcrItem item : neighborItems) {
|
|
||||||
String authority = extractIssuingAuthority(item.getText());
|
|
||||||
if (authority != null) {
|
|
||||||
return authority;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String merged = neighborItems.stream()
|
|
||||||
.map(OcrItem::getText)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.map(this::normalizeText)
|
|
||||||
.collect(Collectors.joining());
|
|
||||||
return extractIssuingAuthority(merged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractIssuingAuthority(String text) {
|
|
||||||
if (text == null || text.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String clean = normalizeText(text)
|
|
||||||
.replace("签发机关", "")
|
|
||||||
.replace("签发機关", "")
|
|
||||||
.replace("签发机関", "")
|
|
||||||
.replace("签发", "");
|
|
||||||
if (clean.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (clean.contains("有效期限") || clean.contains("有效期")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return clean;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValidPeriod findValidPeriod(OcrItem validLabel, List<OcrItem> allItems, Map<OcrBox, OcrItem> itemByBox) {
|
|
||||||
ValidPeriod selfPeriod = extractValidPeriod(validLabel.getText());
|
|
||||||
if (selfPeriod != null) {
|
|
||||||
return selfPeriod;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<OcrBox> boxList = allItems.stream()
|
|
||||||
.map(OcrItem::getOcrBox)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
OcrBox anchorBox = validLabel.getOcrBox();
|
|
||||||
ValidPeriod period = findValidPeriodByDirection(anchorBox, itemByBox, boxList, BoxUtils.Direction.RIGHT, 4);
|
|
||||||
if (period != null) {
|
|
||||||
return period;
|
|
||||||
}
|
|
||||||
period = findValidPeriodByDirection(anchorBox, itemByBox, boxList, BoxUtils.Direction.DOWN, 4);
|
|
||||||
if (period != null) {
|
|
||||||
return period;
|
|
||||||
}
|
|
||||||
period = findValidPeriodByDirection(anchorBox, itemByBox, boxList, BoxUtils.Direction.UP, 2);
|
|
||||||
if (period != null) {
|
|
||||||
return period;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValidPeriod findValidPeriodByDirection(OcrBox anchorBox,
|
|
||||||
Map<OcrBox, OcrItem> itemByBox,
|
|
||||||
List<OcrBox> boxList,
|
|
||||||
BoxUtils.Direction direction,
|
|
||||||
int limit) {
|
|
||||||
List<OcrBox> neighbors = BoxUtils.findNearestBoxes(anchorBox, boxList, direction, limit);
|
|
||||||
if (neighbors == null || neighbors.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<OcrItem> neighborItems = neighbors.stream()
|
|
||||||
.map(itemByBox::get)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
for (OcrItem item : neighborItems) {
|
|
||||||
ValidPeriod period = extractValidPeriod(item.getText());
|
|
||||||
if (period != null) {
|
|
||||||
return period;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String merged = neighborItems.stream()
|
|
||||||
.map(OcrItem::getText)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.map(this::normalizeText)
|
|
||||||
.collect(Collectors.joining());
|
|
||||||
return extractValidPeriod(merged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValidPeriod extractValidPeriod(String text) {
|
|
||||||
if (text == null || text.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String clean = normalizeText(text)
|
|
||||||
.replace("有效期限", "")
|
|
||||||
.replace("有效期", "");
|
|
||||||
|
|
||||||
Matcher matcher = VALID_PERIOD_PATTERN.matcher(clean);
|
|
||||||
if (!matcher.find()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String from = normalizeDate(matcher.group(1), matcher.group(2), matcher.group(3));
|
|
||||||
if (from == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String toRaw = matcher.group(4);
|
|
||||||
String to = "长期".equals(toRaw)
|
|
||||||
? "长期"
|
|
||||||
: normalizeDate(matcher.group(5), matcher.group(6), matcher.group(7));
|
|
||||||
if (to == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ValidPeriod(from, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String normalizeDate(String year, String month, String day) {
|
|
||||||
if (year == null || month == null || day == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return year + "-" + month + "-" + day;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String findIssuingAuthorityByGlobalFallback(List<OcrItem> allItems) {
|
|
||||||
for (OcrItem item : allItems) {
|
|
||||||
String authority = extractIssuingAuthority(item.getText());
|
|
||||||
if (authority != null && (authority.contains("公安局") || authority.contains("分局") || authority.contains("机关"))) {
|
|
||||||
return authority;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValidPeriod findValidPeriodByGlobalFallback(List<OcrItem> allItems) {
|
|
||||||
List<String> texts = new ArrayList<>();
|
|
||||||
for (OcrItem item : allItems) {
|
|
||||||
if (item != null && item.getText() != null) {
|
|
||||||
ValidPeriod period = extractValidPeriod(item.getText());
|
|
||||||
if (period != null) {
|
|
||||||
return period;
|
|
||||||
}
|
|
||||||
texts.add(normalizeText(item.getText()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return extractValidPeriod(String.join("", texts));
|
|
||||||
}
|
|
||||||
|
|
||||||
private OcrItem findLabel(List<OcrItem> allItems, List<String> keywords) {
|
|
||||||
for (OcrItem item : allItems) {
|
|
||||||
String text = item.getText();
|
|
||||||
if (text == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String clean = normalizeText(text);
|
|
||||||
for (String keyword : keywords) {
|
|
||||||
if (clean.contains(keyword)) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String normalizeText(String text) {
|
|
||||||
if (text == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return text.replace(" ", "")
|
|
||||||
.replace("一", "-")
|
|
||||||
.replace("到", "-")
|
|
||||||
.replace("至", "-")
|
|
||||||
.replace("~", "-")
|
|
||||||
.replace("—", "-")
|
|
||||||
.replace("-", "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<OcrItem> filterSmallBoxes(List<OcrItem> items) {
|
|
||||||
List<OcrItem> result = IdCardOcrUtils.filterSmallBoxes(items, 0.15);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<OcrBox, OcrItem> buildBoxItemMap(List<OcrItem> items) {
|
|
||||||
Map<OcrBox, OcrItem> itemByBox = new IdentityHashMap<>();
|
|
||||||
if (items == null) {
|
|
||||||
return itemByBox;
|
|
||||||
}
|
|
||||||
for (OcrItem item : items) {
|
|
||||||
if (item != null && item.getOcrBox() != null) {
|
|
||||||
itemByBox.put(item.getOcrBox(), item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return itemByBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ValidPeriod {
|
|
||||||
private final String validFrom;
|
|
||||||
private final String validTo;
|
|
||||||
|
|
||||||
private ValidPeriod(String validFrom, String validTo) {
|
|
||||||
this.validFrom = validFrom;
|
|
||||||
this.validTo = validTo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,930 +0,0 @@
|
|||||||
package cn.smartjavaai.ocr.idcard;
|
|
||||||
|
|
||||||
import cn.smartjavaai.common.entity.Point;
|
|
||||||
import cn.smartjavaai.ocr.entity.IdCardFrontInfo;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrBox;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrInfo;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrItem;
|
|
||||||
import cn.smartjavaai.ocr.utils.BoxUtils;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.time.DateTimeException;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证正面解析逻辑。
|
|
||||||
*
|
|
||||||
* 从 OCR 结果中解析:
|
|
||||||
* - 姓名(兼容“姓名+值在同一检测框”与“键值分框”的情况)
|
|
||||||
* - 性别
|
|
||||||
* - 民族
|
|
||||||
* - 出生日期
|
|
||||||
* - 公民身份号码
|
|
||||||
* - 住址
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class IdCardFrontParser implements IdCardParser<IdCardFrontInfo> {
|
|
||||||
|
|
||||||
private static final Pattern BIRTHDAY_WITH_SEPARATORS = Pattern.compile(
|
|
||||||
"((?:19|20)\\d{2})[年/._-]?((?:1[0-2])|(?:0?[1-9]))[月/._-]?((?:3[01])|(?:[12]\\d)|(?:0?[1-9]))日?"
|
|
||||||
);
|
|
||||||
|
|
||||||
private static final Pattern BIRTHDAY_COMPACT = Pattern.compile(
|
|
||||||
"((?:19|20)\\d{2})((?:1[0-2])|(?:0[1-9]))((?:3[01])|(?:[12]\\d)|(?:0[1-9]))"
|
|
||||||
);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IdCardFrontInfo parse(OcrInfo ocrInfo) {
|
|
||||||
IdCardFrontInfo info = new IdCardFrontInfo();
|
|
||||||
if (ocrInfo == null || ocrInfo.getLineList() == null || ocrInfo.getLineList().isEmpty()) {
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扁平化所有 item
|
|
||||||
List<OcrItem> allItems = ocrInfo.getLineList().stream()
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.flatMap(List::stream)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
// 过滤明显过小的检测框(噪声)
|
|
||||||
allItems = filterSmallBoxes(allItems);
|
|
||||||
|
|
||||||
// 可用的检测框列表:解析出字段后逐步移除,减少后续遍历量
|
|
||||||
List<OcrItem> remainingItems = new ArrayList<>(allItems);
|
|
||||||
Map<OcrBox, OcrItem> itemByBox = buildBoxItemMap(remainingItems);
|
|
||||||
|
|
||||||
// 1. 姓名(兼容“标签+值同框”以及右侧单独值框)
|
|
||||||
OcrItem nameLabel = findLabel(remainingItems, Arrays.asList("姓名", "姓名:", "姓名:"));
|
|
||||||
if (nameLabel != null) {
|
|
||||||
StringBuilder nameSb = new StringBuilder();
|
|
||||||
if (nameLabel.getText() != null) {
|
|
||||||
nameSb.append(nameLabel.getText());
|
|
||||||
}
|
|
||||||
// 右侧最近一个框(可能是姓名值)
|
|
||||||
OcrItem rightItem = findNearestRightItem(nameLabel, remainingItems, itemByBox);
|
|
||||||
if (rightItem != null && rightItem.getText() != null) {
|
|
||||||
nameSb.append(rightItem.getText());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 过滤:只保留中文字符
|
|
||||||
String nameText = nameSb.toString()
|
|
||||||
.replace(" ", "")
|
|
||||||
.replaceAll("[^\\u4e00-\\u9fa5]", "");
|
|
||||||
|
|
||||||
// 提取"姓名"后面的内容作为名字
|
|
||||||
String name = extractNameAfterLabel(nameText);
|
|
||||||
if (name != null && !name.isEmpty()) {
|
|
||||||
info.setName(name);
|
|
||||||
remainingItems.remove(nameLabel);
|
|
||||||
if (rightItem != null) {
|
|
||||||
remainingItems.remove(rightItem);
|
|
||||||
}
|
|
||||||
itemByBox = buildBoxItemMap(remainingItems);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 性别(优先右 → 上 → 下 → 全局)
|
|
||||||
OcrItem genderLabel = findLabel(remainingItems, Arrays.asList("性别", "性别:", "性别:"));
|
|
||||||
if (genderLabel != null) {
|
|
||||||
String gender = findGenderAroundLabel(genderLabel, remainingItems, itemByBox);
|
|
||||||
if (gender != null) {
|
|
||||||
info.setGender(gender);
|
|
||||||
final String g = gender;
|
|
||||||
// 移除性别相关检测框(若同时包含民族信息则保留用于民族解析)
|
|
||||||
remainingItems.removeIf(it -> {
|
|
||||||
String t = it.getText();
|
|
||||||
if (t == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String clean = t.replace(" ", "");
|
|
||||||
if (clean.contains("性别") && !clean.contains("民族")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
String cg = cleanGender(t);
|
|
||||||
return g.equals(cg) && !clean.contains("民族");
|
|
||||||
});
|
|
||||||
itemByBox = buildBoxItemMap(remainingItems);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 民族(策略同性别)
|
|
||||||
OcrItem ethnicLabel = findLabel(remainingItems, Arrays.asList("民族", "民族:", "民族:"));
|
|
||||||
if (ethnicLabel != null) {
|
|
||||||
String ethnicity = findEthnicityAroundLabel(ethnicLabel, remainingItems, itemByBox);
|
|
||||||
if (ethnicity != null) {
|
|
||||||
info.setEthnicity(ethnicity);
|
|
||||||
final String eth = ethnicity;
|
|
||||||
remainingItems.removeIf(it -> {
|
|
||||||
String t = it.getText();
|
|
||||||
if (t == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String clean = t.replace(" ", "");
|
|
||||||
if (clean.contains("民族")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
String e = extractEthnicity(t);
|
|
||||||
return eth.equals(e);
|
|
||||||
});
|
|
||||||
itemByBox = buildBoxItemMap(remainingItems);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 公民身份号码
|
|
||||||
OcrItem idSourceItem = null;
|
|
||||||
OcrItem idRightItem = null;
|
|
||||||
for (OcrItem item : remainingItems) {
|
|
||||||
String text = normalizeText(item.getText());
|
|
||||||
if (text.contains("公民身份号码") || text.contains("公民身份號碼") || text.toUpperCase().contains("ID")) {
|
|
||||||
String idInBox = extractIdNumber(text);
|
|
||||||
if (idInBox == null) {
|
|
||||||
OcrItem idRight = findNearestRightItem(item, remainingItems, itemByBox);
|
|
||||||
if (idRight != null) {
|
|
||||||
idInBox = extractIdNumber(normalizeText(idRight.getText()));
|
|
||||||
idRightItem = idRight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (idInBox != null) {
|
|
||||||
info.setIdNumber(idInBox);
|
|
||||||
idSourceItem = item;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (idSourceItem != null) {
|
|
||||||
remainingItems.remove(idSourceItem);
|
|
||||||
}
|
|
||||||
if (idRightItem != null) {
|
|
||||||
remainingItems.remove(idRightItem);
|
|
||||||
}
|
|
||||||
if (idSourceItem != null || idRightItem != null) {
|
|
||||||
itemByBox = buildBoxItemMap(remainingItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 出生日期:考虑多框拆分 & 与标签同框等多种情况
|
|
||||||
OcrItem birthLabel = findLabel(remainingItems, Arrays.asList("出生", "出生日期", "出生:", "出生:"));
|
|
||||||
if (birthLabel != null) {
|
|
||||||
String birth = findBirthdayAroundLabel(birthLabel, remainingItems, itemByBox);
|
|
||||||
if (birth != null) {
|
|
||||||
info.setBirthday(birth);
|
|
||||||
final String bFinal = birth;
|
|
||||||
remainingItems.removeIf(it -> {
|
|
||||||
String t = it.getText();
|
|
||||||
if (t == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String clean = t.replace(" ", "");
|
|
||||||
if (clean.contains("出生")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
String ex = extractBirthday(t);
|
|
||||||
return bFinal.equals(ex);
|
|
||||||
});
|
|
||||||
itemByBox = buildBoxItemMap(remainingItems);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果生日没识别到,则尝试从身份证号码中推断
|
|
||||||
if (info.getBirthday() == null && info.getIdNumber() != null && info.getIdNumber().length() >= 14) {
|
|
||||||
String id = info.getIdNumber();
|
|
||||||
String yyyyMMdd = id.substring(6, 14);
|
|
||||||
info.setBirthday(formatBirthdayFromId(yyyyMMdd));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 住址(从“住址”右侧开始,向下若干行拼接)
|
|
||||||
OcrItem addressLabel = findAddressLabel(remainingItems);
|
|
||||||
if (addressLabel != null) {
|
|
||||||
String addr = collectAddress(addressLabel, remainingItems, itemByBox);
|
|
||||||
info.setAddress(addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------- 文本与 Box 相关的工具方法 -----------------
|
|
||||||
|
|
||||||
private String normalizeText(String text) {
|
|
||||||
if (text == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return text.replace(" ", "")
|
|
||||||
.replaceAll("[^0-9Xx\\u4e00-\\u9fa5]", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从包含"姓名"的文本中提取姓名值(提取"姓名"后面的所有中文字符)
|
|
||||||
*/
|
|
||||||
private String extractNameAfterLabel(String text) {
|
|
||||||
if (text == null || text.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int nameIndex = text.indexOf("姓名");
|
|
||||||
if (nameIndex >= 0) {
|
|
||||||
String name = text.substring(nameIndex + 2);
|
|
||||||
return name.isEmpty() ? null : name;
|
|
||||||
}
|
|
||||||
int surnameIndex = text.indexOf("姓");
|
|
||||||
if (surnameIndex >= 0 && surnameIndex < text.length() - 1) {
|
|
||||||
String name = text.substring(surnameIndex + 1);
|
|
||||||
return name.isEmpty() ? null : name;
|
|
||||||
}
|
|
||||||
int givenNameIndex = text.indexOf("名");
|
|
||||||
if (givenNameIndex >= 0 && givenNameIndex < text.length() - 1) {
|
|
||||||
String name = text.substring(givenNameIndex + 1);
|
|
||||||
return name.isEmpty() ? null : name;
|
|
||||||
}
|
|
||||||
// 如果都没找到,返回原文本(可能已经是纯姓名)
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String cleanGender(String text) {
|
|
||||||
if (text == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String clean = text.replace(" ", "");
|
|
||||||
if (clean.contains("男")) {
|
|
||||||
return "男";
|
|
||||||
}
|
|
||||||
if (clean.contains("女")) {
|
|
||||||
return "女";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按“右 → 上 → 下 → 全局”顺序查找性别信息
|
|
||||||
*/
|
|
||||||
private String findGenderAroundLabel(OcrItem genderLabel, List<OcrItem> allItems, Map<OcrBox, OcrItem> itemByBox) {
|
|
||||||
if (genderLabel == null || genderLabel.getOcrBox() == null || allItems == null || allItems.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// 情况一:标签所在框本身包含“性别+男/女”
|
|
||||||
String selfGender = cleanGender(genderLabel.getText());
|
|
||||||
if (selfGender != null) {
|
|
||||||
return selfGender;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<OcrBox> boxList = allItems.stream()
|
|
||||||
.map(OcrItem::getOcrBox)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
OcrBox anchorBox = genderLabel.getOcrBox();
|
|
||||||
|
|
||||||
String gender = findGenderByDirection(anchorBox, itemByBox, boxList, BoxUtils.Direction.RIGHT, 3);
|
|
||||||
if (gender != null) {
|
|
||||||
return gender;
|
|
||||||
}
|
|
||||||
gender = findGenderByDirection(anchorBox, itemByBox, boxList, BoxUtils.Direction.UP, 3);
|
|
||||||
if (gender != null) {
|
|
||||||
return gender;
|
|
||||||
}
|
|
||||||
gender = findGenderByDirection(anchorBox, itemByBox, boxList, BoxUtils.Direction.DOWN, 3);
|
|
||||||
if (gender != null) {
|
|
||||||
return gender;
|
|
||||||
}
|
|
||||||
|
|
||||||
return findNearbyFallback(anchorBox, allItems, this::cleanGender);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按指定方向在若干个最近框中查找性别
|
|
||||||
*/
|
|
||||||
private String findGenderByDirection(OcrBox anchorBox,
|
|
||||||
Map<OcrBox, OcrItem> itemByBox,
|
|
||||||
List<OcrBox> boxList,
|
|
||||||
BoxUtils.Direction direction,
|
|
||||||
int limit) {
|
|
||||||
List<OcrBox> neighbors = BoxUtils.findNearestBoxes(anchorBox, boxList, direction, limit);
|
|
||||||
if (neighbors == null || neighbors.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (OcrBox neighbor : neighbors) {
|
|
||||||
OcrItem item = itemByBox.get(neighbor);
|
|
||||||
if (item == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String g = cleanGender(item.getText());
|
|
||||||
if (g != null) {
|
|
||||||
return g;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按“右 → 上 → 下 → 全局”顺序查找民族信息
|
|
||||||
*/
|
|
||||||
private String findEthnicityAroundLabel(OcrItem ethnicLabel, List<OcrItem> allItems, Map<OcrBox, OcrItem> itemByBox) {
|
|
||||||
if (ethnicLabel == null || ethnicLabel.getOcrBox() == null || allItems == null || allItems.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String selfEthnic = extractEthnicity(ethnicLabel.getText());
|
|
||||||
if (selfEthnic != null) {
|
|
||||||
return selfEthnic;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<OcrBox> boxList = allItems.stream()
|
|
||||||
.map(OcrItem::getOcrBox)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
OcrBox anchorBox = ethnicLabel.getOcrBox();
|
|
||||||
|
|
||||||
String ethnicity = findEthnicityByDirection(anchorBox, itemByBox, boxList, BoxUtils.Direction.RIGHT, 3);
|
|
||||||
if (ethnicity != null) {
|
|
||||||
return ethnicity;
|
|
||||||
}
|
|
||||||
ethnicity = findEthnicityByDirection(anchorBox, itemByBox, boxList, BoxUtils.Direction.UP, 3);
|
|
||||||
if (ethnicity != null) {
|
|
||||||
return ethnicity;
|
|
||||||
}
|
|
||||||
ethnicity = findEthnicityByDirection(anchorBox, itemByBox, boxList, BoxUtils.Direction.DOWN, 3);
|
|
||||||
if (ethnicity != null) {
|
|
||||||
return ethnicity;
|
|
||||||
}
|
|
||||||
|
|
||||||
return findNearbyFallback(anchorBox, allItems, this::extractEthnicity);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按指定方向在若干个最近框中查找民族
|
|
||||||
*/
|
|
||||||
private String findEthnicityByDirection(OcrBox anchorBox,
|
|
||||||
Map<OcrBox, OcrItem> itemByBox,
|
|
||||||
List<OcrBox> boxList,
|
|
||||||
BoxUtils.Direction direction,
|
|
||||||
int limit) {
|
|
||||||
List<OcrBox> neighbors = BoxUtils.findNearestBoxes(anchorBox, boxList, direction, limit);
|
|
||||||
if (neighbors == null || neighbors.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (OcrBox neighbor : neighbors) {
|
|
||||||
OcrItem item = itemByBox.get(neighbor);
|
|
||||||
if (item == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String e = extractEthnicity(item.getText());
|
|
||||||
if (e != null) {
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从文本中提取并纠正民族信息
|
|
||||||
*/
|
|
||||||
private String extractEthnicity(String ocrText) {
|
|
||||||
if (ocrText == null || ocrText.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String cleanText = ocrText.replaceAll("[^\\u4e00-\\u9fa5]", "");
|
|
||||||
cleanText = cleanText.replaceAll("^民族", "");
|
|
||||||
for (String ethnic : IdCardOcrUtils.ETHNIC_SET) {
|
|
||||||
if (cleanText.contains(ethnic)) {
|
|
||||||
return ethnic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 兜底时仅在标签附近寻找合法候选,避免被远处噪声或少数民族文字误带偏。
|
|
||||||
*/
|
|
||||||
private String findNearbyFallback(OcrBox anchorBox,
|
|
||||||
List<OcrItem> allItems,
|
|
||||||
Function<String, String> extractor) {
|
|
||||||
if (anchorBox == null || allItems == null || allItems.isEmpty() || extractor == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
double anchorWidth = boxWidth(anchorBox);
|
|
||||||
double anchorHeight = boxHeight(anchorBox);
|
|
||||||
Point anchorCenter = boxCenter(anchorBox);
|
|
||||||
|
|
||||||
OcrItem bestItem = null;
|
|
||||||
double bestScore = Double.MAX_VALUE;
|
|
||||||
|
|
||||||
for (OcrItem item : allItems) {
|
|
||||||
if (item == null || item.getOcrBox() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String extracted = extractor.apply(item.getText());
|
|
||||||
if (extracted == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Point candidateCenter = boxCenter(item.getOcrBox());
|
|
||||||
double dx = Math.abs(candidateCenter.getX() - anchorCenter.getX());
|
|
||||||
double dy = Math.abs(candidateCenter.getY() - anchorCenter.getY());
|
|
||||||
|
|
||||||
// 性别/民族通常紧邻标签,限制在局部窗口内做兜底。
|
|
||||||
if (dx > anchorWidth * 5.0 || dy > anchorHeight * 2.5) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
double score = dy * 2.0 + dx;
|
|
||||||
if (score < bestScore) {
|
|
||||||
bestScore = score;
|
|
||||||
bestItem = item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bestItem == null ? null : extractor.apply(bestItem.getText());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按“自身 → 右 → 下 → 上 → 全局”顺序查找出生日期
|
|
||||||
*/
|
|
||||||
|
|
||||||
private String findBirthdayAroundLabel(OcrItem birthLabel, List<OcrItem> allItems, Map<OcrBox, OcrItem> itemByBox) {
|
|
||||||
if (birthLabel == null || birthLabel.getOcrBox() == null || allItems == null || allItems.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String fromSelf = extractBirthday(birthLabel.getText());
|
|
||||||
if (fromSelf != null) {
|
|
||||||
return fromSelf;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<OcrBox> boxList = allItems.stream()
|
|
||||||
.map(OcrItem::getOcrBox)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
OcrBox anchorBox = birthLabel.getOcrBox();
|
|
||||||
|
|
||||||
String fromRight = findBirthdayByDirection(anchorBox, itemByBox, boxList, BoxUtils.Direction.RIGHT, 4);
|
|
||||||
if (fromRight != null) {
|
|
||||||
return fromRight;
|
|
||||||
}
|
|
||||||
|
|
||||||
String fromDown = findBirthdayByDirection(anchorBox, itemByBox, boxList, BoxUtils.Direction.DOWN, 4);
|
|
||||||
if (fromDown != null) {
|
|
||||||
return fromDown;
|
|
||||||
}
|
|
||||||
|
|
||||||
String fromUp = findBirthdayByDirection(anchorBox, itemByBox, boxList, BoxUtils.Direction.UP, 3);
|
|
||||||
if (fromUp != null) {
|
|
||||||
return fromUp;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (OcrItem item : allItems) {
|
|
||||||
String b = extractBirthday(item.getText());
|
|
||||||
if (b != null) {
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按指定方向在若干最近框中尝试组合/单独解析出生日期
|
|
||||||
*/
|
|
||||||
private String findBirthdayByDirection(OcrBox anchorBox,
|
|
||||||
Map<OcrBox, OcrItem> itemByBox,
|
|
||||||
List<OcrBox> boxList,
|
|
||||||
BoxUtils.Direction direction,
|
|
||||||
int limit) {
|
|
||||||
List<OcrBox> neighbors = BoxUtils.findNearestBoxes(anchorBox, boxList, direction, limit);
|
|
||||||
if (neighbors == null || neighbors.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<OcrItem> neighborItems = neighbors.stream()
|
|
||||||
.map(itemByBox::get)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (neighborItems.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (OcrItem item : neighborItems) {
|
|
||||||
String b = extractBirthday(item.getText());
|
|
||||||
if (b != null) {
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (OcrItem item : neighborItems) {
|
|
||||||
if (item.getText() != null) {
|
|
||||||
sb.append(item.getText().replace(" ", ""));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String merged = sb.toString();
|
|
||||||
if (!merged.isEmpty()) {
|
|
||||||
String b = extractBirthday(merged);
|
|
||||||
if (b != null) {
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从文本中提取身份证号(优先 18 位二代证)
|
|
||||||
*/
|
|
||||||
private String extractIdNumber(String text) {
|
|
||||||
if (text == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String candidate = text.toUpperCase();
|
|
||||||
Matcher m = Pattern.compile("[0-9X]{15,18}").matcher(candidate);
|
|
||||||
while (m.find()) {
|
|
||||||
String id = m.group();
|
|
||||||
if (id.length() == 18) {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从文本中提取生日(标准化为 yyyy-MM-dd)
|
|
||||||
*/
|
|
||||||
private String extractBirthday(String text) {
|
|
||||||
if (text == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String clean = text.replace(" ", "");
|
|
||||||
|
|
||||||
Matcher m1 = BIRTHDAY_WITH_SEPARATORS.matcher(clean);
|
|
||||||
if (m1.find()) {
|
|
||||||
String birthday = normalizeBirthday(m1.group(1), m1.group(2), m1.group(3));
|
|
||||||
if (birthday != null) {
|
|
||||||
return birthday;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Matcher m2 = BIRTHDAY_COMPACT.matcher(clean);
|
|
||||||
if (m2.find()) {
|
|
||||||
String birthday = normalizeBirthday(m2.group(1), m2.group(2), m2.group(3));
|
|
||||||
if (birthday != null) {
|
|
||||||
return birthday;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String normalizeBirthday(String raw) {
|
|
||||||
String clean = raw.replace("年", "-")
|
|
||||||
.replace("月", "-")
|
|
||||||
.replace("日", "")
|
|
||||||
.replace("/", "-")
|
|
||||||
.replace(".", "-");
|
|
||||||
String[] parts = clean.split("-");
|
|
||||||
if (parts.length != 3) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String y = parts[0];
|
|
||||||
String m = parts[1].length() == 1 ? "0" + parts[1] : parts[1];
|
|
||||||
String d = parts[2].length() == 1 ? "0" + parts[2] : parts[2];
|
|
||||||
return y + "-" + m + "-" + d;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String normalizeBirthday(String year, String month, String day) {
|
|
||||||
if (year == null || month == null || day == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String normalizedMonth = month.length() == 1 ? "0" + month : month;
|
|
||||||
String normalizedDay = day.length() == 1 ? "0" + day : day;
|
|
||||||
try {
|
|
||||||
LocalDate date = LocalDate.of(
|
|
||||||
Integer.parseInt(year),
|
|
||||||
Integer.parseInt(normalizedMonth),
|
|
||||||
Integer.parseInt(normalizedDay)
|
|
||||||
);
|
|
||||||
return date.toString();
|
|
||||||
} catch (DateTimeException | NumberFormatException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String formatBirthdayFromId(String yyyymmdd) {
|
|
||||||
if (yyyymmdd == null || yyyymmdd.length() != 8) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String y = yyyymmdd.substring(0, 4);
|
|
||||||
String m = yyyymmdd.substring(4, 6);
|
|
||||||
String d = yyyymmdd.substring(6, 8);
|
|
||||||
return y + "-" + m + "-" + d;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Point boxCenter(OcrBox box) {
|
|
||||||
float[] pts = box.toFloatArray();
|
|
||||||
float cx = (pts[0] + pts[2] + pts[4] + pts[6]) / 4;
|
|
||||||
float cy = (pts[1] + pts[3] + pts[5] + pts[7]) / 4;
|
|
||||||
return new Point(cx, cy);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double boxWidth(OcrBox box) {
|
|
||||||
float[] pts = box.toFloatArray();
|
|
||||||
float minX = Math.min(Math.min(pts[0], pts[2]), Math.min(pts[4], pts[6]));
|
|
||||||
float maxX = Math.max(Math.max(pts[0], pts[2]), Math.max(pts[4], pts[6]));
|
|
||||||
return Math.max(1.0, maxX - minX);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double boxHeight(OcrBox box) {
|
|
||||||
float[] pts = box.toFloatArray();
|
|
||||||
float minY = Math.min(Math.min(pts[1], pts[3]), Math.min(pts[5], pts[7]));
|
|
||||||
float maxY = Math.max(Math.max(pts[1], pts[3]), Math.max(pts[5], pts[7]));
|
|
||||||
return Math.max(1.0, maxY - minY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 过滤掉面积明显小于整体的检测框,粗略去噪
|
|
||||||
*/
|
|
||||||
private List<OcrItem> filterSmallBoxes(List<OcrItem> items) {
|
|
||||||
List<OcrItem> result = IdCardOcrUtils.filterSmallBoxes(items, 0.15);
|
|
||||||
log.debug("身份证正面解析:过滤小框完成,原始数量={},过滤后数量={}", items == null ? 0 : items.size(), result == null ? 0 : result.size());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基于 BoxUtils,从锚点右侧找到最近的一个文本框
|
|
||||||
*/
|
|
||||||
private OcrItem findNearestRightItem(OcrItem anchor, List<OcrItem> allItems, Map<OcrBox, OcrItem> itemByBox) {
|
|
||||||
if (anchor == null || anchor.getOcrBox() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
List<OcrBox> boxList = allItems.stream()
|
|
||||||
.map(OcrItem::getOcrBox)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
OcrBox nearest = BoxUtils.findNearestBox(anchor.getOcrBox(), boxList, BoxUtils.Direction.RIGHT);
|
|
||||||
if (nearest == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return itemByBox.get(nearest);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查找包含标签关键字的 item
|
|
||||||
*/
|
|
||||||
private OcrItem findLabel(List<OcrItem> allItems, List<String> keywords) {
|
|
||||||
for (OcrItem item : allItems) {
|
|
||||||
String text = item.getText();
|
|
||||||
if (text == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String clean = text.replace(" ", "");
|
|
||||||
for (String kw : keywords) {
|
|
||||||
if (clean.contains(kw)) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private OcrItem findAddressLabel(List<OcrItem> allItems) {
|
|
||||||
OcrItem direct = findLabel(allItems, Arrays.asList("住址", "地址", "住址:", "住址:"));
|
|
||||||
if (direct != null) {
|
|
||||||
return direct;
|
|
||||||
}
|
|
||||||
if (allItems == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (OcrItem item : allItems) {
|
|
||||||
String text = item.getText();
|
|
||||||
if (text == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String clean = text.replace(" ", "");
|
|
||||||
if (looksLikeAddressLabel(clean)) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean looksLikeAddressLabel(String text) {
|
|
||||||
if (text == null || text.length() < 2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (text.contains("址") && (text.startsWith("住") || text.startsWith("佳") || text.startsWith("往"))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 拼接地址:
|
|
||||||
* 1. 从“住址”右侧开始,整行向右拼接多个检测框
|
|
||||||
* 2. 继续向下拼接若干行同一列附近的文本
|
|
||||||
* 3. 若某一行疑似“公民身份号码”行,则终止
|
|
||||||
*/
|
|
||||||
private String collectAddress(OcrItem addressLabel, List<OcrItem> allItems, Map<OcrBox, OcrItem> itemByBox) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
if (addressLabel == null || addressLabel.getOcrBox() == null || allItems == null || allItems.isEmpty()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
List<OcrBox> boxList = allItems.stream()
|
|
||||||
.map(OcrItem::getOcrBox)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
Set<OcrBox> usedBoxes = new HashSet<>();
|
|
||||||
|
|
||||||
String labelAddressText = extractAddressAfterLabel(addressLabel.getText());
|
|
||||||
if (!labelAddressText.isEmpty()) {
|
|
||||||
appendMergedText(sb, labelAddressText);
|
|
||||||
usedBoxes.add(addressLabel.getOcrBox());
|
|
||||||
}
|
|
||||||
|
|
||||||
OcrItem firstLineItem = findNearestRightItem(addressLabel, allItems, itemByBox);
|
|
||||||
if (firstLineItem != null && firstLineItem.getOcrBox() != null) {
|
|
||||||
String firstLineText = buildAddressLine(firstLineItem.getOcrBox(), itemByBox, boxList, usedBoxes);
|
|
||||||
if (!firstLineText.isEmpty() && !isIdNumberLine(firstLineText)) {
|
|
||||||
appendMergedText(sb, firstLineText);
|
|
||||||
} else if (isIdNumberLine(firstLineText)) {
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OcrBox current = firstLineItem != null && firstLineItem.getOcrBox() != null
|
|
||||||
? firstLineItem.getOcrBox()
|
|
||||||
: addressLabel.getOcrBox();
|
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++) {
|
|
||||||
List<OcrBox> downs = BoxUtils.findNearestBoxes(
|
|
||||||
current,
|
|
||||||
boxList,
|
|
||||||
BoxUtils.Direction.DOWN,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
if (downs == null || downs.isEmpty()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
OcrBox down = downs.get(0);
|
|
||||||
if (!isLikelyAddressContinuation(current, down, itemByBox)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
String lineText = buildAddressLine(down, itemByBox, boxList, usedBoxes);
|
|
||||||
if (lineText.isEmpty()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (isIdNumberLine(lineText)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
appendMergedText(sb, lineText);
|
|
||||||
current = down;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractAddressAfterLabel(String text) {
|
|
||||||
if (text == null || text.isEmpty()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
String clean = text.replace(" ", "");
|
|
||||||
int idx = clean.indexOf("住址");
|
|
||||||
if (idx >= 0) {
|
|
||||||
return clean.substring(idx + 2);
|
|
||||||
}
|
|
||||||
idx = clean.indexOf("佳址");
|
|
||||||
if (idx >= 0) {
|
|
||||||
return clean.substring(idx + 2);
|
|
||||||
}
|
|
||||||
idx = clean.indexOf("往址");
|
|
||||||
if (idx >= 0) {
|
|
||||||
return clean.substring(idx + 2);
|
|
||||||
}
|
|
||||||
idx = clean.indexOf("地址");
|
|
||||||
if (idx >= 0) {
|
|
||||||
return clean.substring(idx + 2);
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void appendMergedText(StringBuilder sb, String nextText) {
|
|
||||||
if (nextText == null || nextText.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (sb.length() == 0) {
|
|
||||||
sb.append(nextText);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String existing = sb.toString();
|
|
||||||
int maxOverlap = Math.min(existing.length(), nextText.length());
|
|
||||||
for (int overlap = maxOverlap; overlap > 0; overlap--) {
|
|
||||||
if (existing.regionMatches(existing.length() - overlap, nextText, 0, overlap)) {
|
|
||||||
sb.append(nextText.substring(overlap));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sb.append(nextText);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isLikelyAddressContinuation(OcrBox current, OcrBox candidate, Map<OcrBox, OcrItem> itemByBox) {
|
|
||||||
if (current == null || candidate == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
OcrItem candidateItem = itemByBox.get(candidate);
|
|
||||||
if (candidateItem == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String text = candidateItem.getText();
|
|
||||||
if (text == null || text.trim().isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String compact = text.replace(" ", "");
|
|
||||||
if (!compact.matches(".*[\\u4e00-\\u9fa5].*")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (compact.matches("[A-Za-z]+")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
double currentBottom = Math.max(Math.max(current.getBottomLeft().getY(), current.getBottomRight().getY()),
|
|
||||||
Math.max(current.getTopLeft().getY(), current.getTopRight().getY()));
|
|
||||||
double candidateTop = Math.min(Math.min(candidate.getTopLeft().getY(), candidate.getTopRight().getY()),
|
|
||||||
Math.min(candidate.getBottomLeft().getY(), candidate.getBottomRight().getY()));
|
|
||||||
double verticalGap = candidateTop - currentBottom;
|
|
||||||
double allowedGap = Math.max(boxHeight(current), boxHeight(candidate)) * 1.2;
|
|
||||||
if (verticalGap > allowedGap) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
double currentCenterX = boxCenter(current).getX();
|
|
||||||
double candidateCenterX = boxCenter(candidate).getX();
|
|
||||||
double centerDeltaX = Math.abs(candidateCenterX - currentCenterX);
|
|
||||||
double allowedDeltaX = Math.max(boxWidth(current), boxWidth(candidate)) * 0.9;
|
|
||||||
return centerDeltaX <= allowedDeltaX;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据某一行的起始框,向右拼接同一行上的多个检测框文本
|
|
||||||
*/
|
|
||||||
private String buildAddressLine(OcrBox rowAnchor,
|
|
||||||
Map<OcrBox, OcrItem> itemByBox,
|
|
||||||
List<OcrBox> boxList,
|
|
||||||
Set<OcrBox> usedBoxes) {
|
|
||||||
if (rowAnchor == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
|
|
||||||
OcrItem anchorItem = itemByBox.get(rowAnchor);
|
|
||||||
if (anchorItem != null && anchorItem.getText() != null && !usedBoxes.contains(rowAnchor)) {
|
|
||||||
sb.append(anchorItem.getText().replace(" ", ""));
|
|
||||||
usedBoxes.add(rowAnchor);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<OcrBox> rightBoxes = BoxUtils.findNearestBoxes(rowAnchor, boxList, BoxUtils.Direction.RIGHT, 8);
|
|
||||||
if (rightBoxes != null && !rightBoxes.isEmpty()) {
|
|
||||||
for (OcrBox rb : rightBoxes) {
|
|
||||||
if (usedBoxes.contains(rb)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
OcrItem item = itemByBox.get(rb);
|
|
||||||
if (item != null && item.getText() != null) {
|
|
||||||
sb.append(item.getText().replace(" ", ""));
|
|
||||||
usedBoxes.add(rb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断一行文本是否疑似“公民身份号码”行
|
|
||||||
*/
|
|
||||||
private boolean isIdNumberLine(String text) {
|
|
||||||
if (text == null || text.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String norm = normalizeText(text);
|
|
||||||
if (norm.contains("公民身份号码") || norm.contains("公民身份號碼") || norm.toUpperCase().contains("ID")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return extractIdNumber(norm) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<OcrBox, OcrItem> buildBoxItemMap(List<OcrItem> items) {
|
|
||||||
Map<OcrBox, OcrItem> itemByBox = new IdentityHashMap<>();
|
|
||||||
if (items == null) {
|
|
||||||
return itemByBox;
|
|
||||||
}
|
|
||||||
for (OcrItem item : items) {
|
|
||||||
if (item != null && item.getOcrBox() != null) {
|
|
||||||
itemByBox.put(item.getOcrBox(), item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return itemByBox;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package cn.smartjavaai.ocr.idcard;
|
|
||||||
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrBox;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrItem;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证 OCR 解析相关的通用工具。
|
|
||||||
*/
|
|
||||||
final class IdCardOcrUtils {
|
|
||||||
|
|
||||||
static final Set<String> ETHNIC_SET = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
|
||||||
"汉", "蒙古", "回", "藏", "维吾尔", "苗", "彝", "壮", "布依", "朝鲜", "满", "侗",
|
|
||||||
"瑶", "白", "土家", "哈尼", "哈萨克", "傣", "黎", "傈僳", "佤", "畲", "高山",
|
|
||||||
"拉祜", "水", "东乡", "纳西", "景颇", "柯尔克孜", "土", "达斡尔", "仫佬",
|
|
||||||
"羌", "布朗", "撒拉", "毛南", "仡佬", "锡伯", "阿昌", "普米", "塔吉克", "怒",
|
|
||||||
"乌孜别克", "俄罗斯", "鄂温克", "德昂", "保安", "裕固", "京", "塔塔尔", "独龙",
|
|
||||||
"鄂伦春", "赫哲", "门巴", "珞巴", "基诺"
|
|
||||||
)));
|
|
||||||
|
|
||||||
private IdCardOcrUtils() {
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<OcrItem> filterSmallBoxes(List<OcrItem> items, double factor) {
|
|
||||||
if (items == null || items.isEmpty()) {
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Double> areas = items.stream()
|
|
||||||
.map(OcrItem::getOcrBox)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.map(IdCardOcrUtils::estimateBoxArea)
|
|
||||||
.sorted()
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (areas.size() < 5) {
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
double median = areas.get(areas.size() / 2);
|
|
||||||
double threshold = median * factor;
|
|
||||||
List<OcrItem> result = items.stream()
|
|
||||||
.filter(it -> it.getOcrBox() == null || estimateBoxArea(it.getOcrBox()) >= threshold)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
return result.isEmpty() ? items : result;
|
|
||||||
}
|
|
||||||
|
|
||||||
static double estimateBoxArea(OcrBox box) {
|
|
||||||
if (box == null) {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
float[] pts = box.toFloatArray();
|
|
||||||
float minX = Math.min(Math.min(pts[0], pts[2]), Math.min(pts[4], pts[6]));
|
|
||||||
float minY = Math.min(Math.min(pts[1], pts[3]), Math.min(pts[5], pts[7]));
|
|
||||||
float maxX = Math.max(Math.max(pts[0], pts[2]), Math.max(pts[4], pts[6]));
|
|
||||||
float maxY = Math.max(Math.max(pts[1], pts[3]), Math.max(pts[5], pts[7]));
|
|
||||||
double width = Math.max(1.0, maxX - minX);
|
|
||||||
double height = Math.max(1.0, maxY - minY);
|
|
||||||
return width * height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package cn.smartjavaai.ocr.idcard;
|
|
||||||
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrInfo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证解析接口,用于从 OCR 结果中解析结构化字段。
|
|
||||||
*/
|
|
||||||
public interface IdCardParser<T> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 OCR 结果中解析出指定类型的身份证信息
|
|
||||||
*/
|
|
||||||
T parse(OcrInfo ocrInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package cn.smartjavaai.ocr.idcard;
|
|
||||||
|
|
||||||
import ai.djl.modality.cv.Image;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrInfo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证预处理调试监听器。
|
|
||||||
*
|
|
||||||
* 在预处理或识别流程中,只有当某个阶段真的产出了中间结果时,才会回调当前图像,
|
|
||||||
* 便于测试环境中将中间结果保存到磁盘进行对比和调试。
|
|
||||||
*
|
|
||||||
* 生产环境可不设置监听器,完全不影响正常流程。
|
|
||||||
*/
|
|
||||||
public interface IdCardPreprocessListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 方向矫正完成后回调。
|
|
||||||
* 如果方向检测结果为 0 且未发生旋转,则不会调用。
|
|
||||||
*/
|
|
||||||
default void onAfterDirection(Image image) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OCR 识别完成后回调。
|
|
||||||
*
|
|
||||||
* 传入的 image 为预处理后的图片副本,已绘制 OCR 检测框,便于直接保存调试结果。
|
|
||||||
* 如果识别流程失败或未进入识别阶段,则不会调用。
|
|
||||||
*/
|
|
||||||
default void onAfterRecognize(Image image, OcrInfo ocrInfo) {}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package cn.smartjavaai.ocr.idcard;
|
|
||||||
|
|
||||||
import ai.djl.modality.cv.Image;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrBox;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.Accessors;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证预处理结果。
|
|
||||||
*
|
|
||||||
* - processedImage: 预处理后的图像;若无需旋转,则通常与原图一致
|
|
||||||
* - reusableBoxes: 可复用的文本检测框;仅在无需旋转时可直接复用
|
|
||||||
* - rotated: 是否进行了整图旋转
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Accessors(chain = true)
|
|
||||||
public class IdCardPreprocessResult {
|
|
||||||
|
|
||||||
private Image processedImage;
|
|
||||||
|
|
||||||
private List<OcrBox> reusableBoxes;
|
|
||||||
|
|
||||||
private boolean rotated;
|
|
||||||
}
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
package cn.smartjavaai.ocr.idcard;
|
|
||||||
|
|
||||||
import ai.djl.modality.cv.Image;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrBox;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrItem;
|
|
||||||
import cn.smartjavaai.ocr.enums.AngleEnum;
|
|
||||||
import cn.smartjavaai.ocr.model.common.detect.OcrCommonDetModel;
|
|
||||||
import cn.smartjavaai.ocr.model.common.direction.OcrDirectionModel;
|
|
||||||
import cn.smartjavaai.ocr.utils.OcrUtils;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.opencv.core.Mat;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证图像预处理:方向矫正(0/90/180/270°)。
|
|
||||||
*
|
|
||||||
* - 方向矫正:使用 OcrDirectionModel 检测整图方向并旋转至正向;
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class IdCardPreprocessor {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 预处理:
|
|
||||||
* 1. 方向矫正(若提供 directionModel):纠正 0/90/180/270° 整体方向;
|
|
||||||
* 2. 若提供 listener,则在阶段结束后回调当前图像,便于测试保存中间结果。
|
|
||||||
*/
|
|
||||||
public Image preprocess(Image src,
|
|
||||||
OcrDirectionModel directionModel,
|
|
||||||
IdCardPreprocessListener listener) {
|
|
||||||
if (src == null) {
|
|
||||||
return src;
|
|
||||||
}
|
|
||||||
if (directionModel != null) {
|
|
||||||
return applyDirectionCorrection(src, directionModel, listener);
|
|
||||||
}
|
|
||||||
return src;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 预处理(不关心调试监听器的常规调用入口)。
|
|
||||||
*/
|
|
||||||
public Image preprocess(Image src,
|
|
||||||
OcrDirectionModel directionModel) {
|
|
||||||
return preprocess(src, directionModel, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 预处理增强版:
|
|
||||||
* 1. 先做文本检测
|
|
||||||
* 2. 过滤明显过小的文本框,降低方向判断噪声
|
|
||||||
* 3. 使用已有文本框做方向检测
|
|
||||||
* 4. 若无需旋转,则直接返回可复用的检测框
|
|
||||||
* 5. 若需要旋转,则返回旋转后的图像,检测框不复用
|
|
||||||
*/
|
|
||||||
public IdCardPreprocessResult preprocess(Image src,
|
|
||||||
OcrCommonDetModel textDetModel,
|
|
||||||
OcrDirectionModel directionModel,
|
|
||||||
IdCardPreprocessListener listener) {
|
|
||||||
IdCardPreprocessResult result = new IdCardPreprocessResult()
|
|
||||||
.setProcessedImage(src)
|
|
||||||
.setRotated(false);
|
|
||||||
if (src == null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (textDetModel == null || directionModel == null) {
|
|
||||||
if (directionModel != null) {
|
|
||||||
result.setProcessedImage(preprocess(src, directionModel, listener));
|
|
||||||
result.setRotated(result.getProcessedImage() != src);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
Mat srcMat = null;
|
|
||||||
try {
|
|
||||||
srcMat = cn.smartjavaai.common.utils.ImageUtils.toMat(src).clone();
|
|
||||||
List<OcrBox> detectedBoxes = textDetModel.detect(src);
|
|
||||||
List<OcrBox> filteredBoxes = filterSmallBoxes(detectedBoxes);
|
|
||||||
if (filteredBoxes.isEmpty()) {
|
|
||||||
result.setReusableBoxes(detectedBoxes);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
List<OcrItem> directionItems = directionModel.detect(filteredBoxes, srcMat);
|
|
||||||
AngleEnum dominant = dominantAngle(directionItems);
|
|
||||||
if (dominant != null && dominant != AngleEnum.ANGLE_0) {
|
|
||||||
log.debug("方向检测结果为 {},执行整图旋转矫正。", dominant);
|
|
||||||
Image rotated = OcrUtils.rotateImg(src, dominant);
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onAfterDirection(rotated);
|
|
||||||
}
|
|
||||||
return result.setProcessedImage(rotated)
|
|
||||||
.setReusableBoxes(null)
|
|
||||||
.setRotated(true);
|
|
||||||
}
|
|
||||||
return result.setReusableBoxes(detectedBoxes);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("身份证方向矫正失败,使用原图继续。", e);
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
if (srcMat != null) {
|
|
||||||
srcMat.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用方向模型检测整图方向(0/90/180/270°),并将图片旋转至正向。
|
|
||||||
*/
|
|
||||||
private Image applyDirectionCorrection(Image src,
|
|
||||||
OcrDirectionModel directionModel,
|
|
||||||
IdCardPreprocessListener listener) {
|
|
||||||
if (src == null || directionModel == null) {
|
|
||||||
return src;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
long detectStart = System.nanoTime();
|
|
||||||
List<OcrItem> items = directionModel.detect(src);
|
|
||||||
log.debug("身份证方向模型调用耗时={}ms, 检测框数量={}", elapsedMillis(detectStart), items == null ? 0 : items.size());
|
|
||||||
log.debug("方向检测结果:{}", items);
|
|
||||||
AngleEnum dominant = dominantAngle(items);
|
|
||||||
if (dominant != null && dominant != AngleEnum.ANGLE_0) {
|
|
||||||
log.info("方向检测结果为 {},执行整图旋转矫正。", dominant);
|
|
||||||
long rotateStart = System.nanoTime();
|
|
||||||
Image rotated = OcrUtils.rotateImg(src, dominant);
|
|
||||||
log.debug("身份证整图旋转耗时={}ms", elapsedMillis(rotateStart));
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onAfterDirection(rotated);
|
|
||||||
}
|
|
||||||
return rotated;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("身份证方向矫正失败,使用原图继续。", e);
|
|
||||||
}
|
|
||||||
return src;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从方向检测结果中取主角度:
|
|
||||||
* 1. 先过滤掉明显的小框,减少碎框噪声;
|
|
||||||
* 2. 再按 面积 * score 做加权投票;
|
|
||||||
* 3. 若过滤后为空,则退化为对全部框做加权投票。
|
|
||||||
*/
|
|
||||||
private AngleEnum dominantAngle(List<OcrItem> items) {
|
|
||||||
if (items == null || items.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
double maxArea = 0.0;
|
|
||||||
for (OcrItem item : items) {
|
|
||||||
if (item == null || item.getAngle() == null || item.getOcrBox() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
maxArea = Math.max(maxArea, estimateBoxArea(item.getOcrBox()));
|
|
||||||
}
|
|
||||||
if (maxArea <= 0.0) {
|
|
||||||
return weightedVote(items, 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
double minArea = maxArea * 0.15;
|
|
||||||
AngleEnum dominant = weightedVote(items, minArea);
|
|
||||||
if (dominant != null) {
|
|
||||||
return dominant;
|
|
||||||
}
|
|
||||||
return weightedVote(items, 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private AngleEnum weightedVote(List<OcrItem> items, double minArea) {
|
|
||||||
AngleEnum first = null;
|
|
||||||
double weight0 = 0.0, weight90 = 0.0, weight180 = 0.0, weight270 = 0.0;
|
|
||||||
for (OcrItem item : items) {
|
|
||||||
if (item == null || item.getAngle() == null || item.getOcrBox() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
double area = estimateBoxArea(item.getOcrBox());
|
|
||||||
if (area < minArea) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
double score = item.getScore() > 0 ? item.getScore() : 1.0;
|
|
||||||
double weight = area * score;
|
|
||||||
AngleEnum angle = item.getAngle();
|
|
||||||
if (first == null) {
|
|
||||||
first = angle;
|
|
||||||
}
|
|
||||||
switch (angle) {
|
|
||||||
case ANGLE_0: weight0 += weight; break;
|
|
||||||
case ANGLE_90: weight90 += weight; break;
|
|
||||||
case ANGLE_180: weight180 += weight; break;
|
|
||||||
case ANGLE_270: weight270 += weight; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
double maxWeight = Math.max(Math.max(weight0, weight90), Math.max(weight180, weight270));
|
|
||||||
if (maxWeight <= 0.0) {
|
|
||||||
return first;
|
|
||||||
}
|
|
||||||
if (Double.compare(weight90, maxWeight) == 0) return AngleEnum.ANGLE_90;
|
|
||||||
if (Double.compare(weight270, maxWeight) == 0) return AngleEnum.ANGLE_270;
|
|
||||||
if (Double.compare(weight180, maxWeight) == 0) return AngleEnum.ANGLE_180;
|
|
||||||
if (Double.compare(weight0, maxWeight) == 0) return AngleEnum.ANGLE_0;
|
|
||||||
return first;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<OcrBox> filterSmallBoxes(List<OcrBox> boxes) {
|
|
||||||
if (boxes == null || boxes.isEmpty()) {
|
|
||||||
return boxes;
|
|
||||||
}
|
|
||||||
List<OcrItem> items = new ArrayList<>(boxes.size());
|
|
||||||
for (OcrBox box : boxes) {
|
|
||||||
items.add(new OcrItem(box, AngleEnum.ANGLE_0, 1.0f));
|
|
||||||
}
|
|
||||||
List<OcrItem> filteredItems = IdCardOcrUtils.filterSmallBoxes(items, 0.15);
|
|
||||||
List<OcrBox> filteredBoxes = new ArrayList<>(filteredItems.size());
|
|
||||||
for (OcrItem item : filteredItems) {
|
|
||||||
if (item != null && item.getOcrBox() != null) {
|
|
||||||
filteredBoxes.add(item.getOcrBox());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filteredBoxes.isEmpty() ? boxes : filteredBoxes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double estimateBoxArea(OcrBox box) {
|
|
||||||
return IdCardOcrUtils.estimateBoxArea(box);
|
|
||||||
}
|
|
||||||
|
|
||||||
private long elapsedMillis(long startNanos) {
|
|
||||||
return (System.nanoTime() - startNanos) / 1_000_000;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package cn.smartjavaai.ocr.idcard;
|
|
||||||
|
|
||||||
import ai.djl.modality.cv.Image;
|
|
||||||
import cn.smartjavaai.ocr.entity.IdCardBackInfo;
|
|
||||||
import cn.smartjavaai.ocr.entity.IdCardFrontInfo;
|
|
||||||
import cn.smartjavaai.ocr.entity.IdCardInfo;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrInfo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证 OCR 识别服务接口
|
|
||||||
*
|
|
||||||
* 支持:
|
|
||||||
* 1. 直接传入图片,由服务内部完成预处理 + OCR + 解析;
|
|
||||||
* 2. 已有 OCR 结果的情况下,仅做单面结构化解析。
|
|
||||||
*/
|
|
||||||
public interface IdCardRecognizer {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 识别身份证正面(从图片开始)
|
|
||||||
*/
|
|
||||||
IdCardFrontInfo recognizeFront(Image image);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 识别身份证反面(从图片开始)
|
|
||||||
*/
|
|
||||||
IdCardBackInfo recognizeBack(Image image);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 识别身份证正面(仅结构化已有 OCR 结果)
|
|
||||||
*/
|
|
||||||
IdCardFrontInfo recognizeFront(OcrInfo ocrInfo);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 识别身份证反面(仅结构化已有 OCR 结果)
|
|
||||||
*/
|
|
||||||
IdCardBackInfo recognizeBack(OcrInfo ocrInfo);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同时识别身份证正反面(分别传入两面图片)
|
|
||||||
*/
|
|
||||||
IdCardInfo recognizeBoth(Image frontImage, Image backImage);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package cn.smartjavaai.ocr.idcard;
|
|
||||||
|
|
||||||
import cn.smartjavaai.ocr.entity.IdCardBackInfo;
|
|
||||||
import cn.smartjavaai.ocr.entity.IdCardFrontInfo;
|
|
||||||
import cn.smartjavaai.ocr.entity.IdCardInfo;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 身份证字段校验工具。
|
|
||||||
*
|
|
||||||
* 提供:
|
|
||||||
* - 正面字段完整性与格式校验
|
|
||||||
* - 反面字段简单格式校验(预留)
|
|
||||||
* - 身份证号码校验位校验
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class IdCardValidator {
|
|
||||||
|
|
||||||
public boolean validateFront(IdCardFrontInfo info) {
|
|
||||||
if (info == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
boolean nameOk = info.getName() != null
|
|
||||||
&& info.getName().length() >= 2
|
|
||||||
&& info.getName().length() <= 4;
|
|
||||||
boolean genderOk = "男".equals(info.getGender()) || "女".equals(info.getGender());
|
|
||||||
boolean ethnicOk = info.getEthnicity() != null && IdCardOcrUtils.ETHNIC_SET.contains(info.getEthnicity());
|
|
||||||
boolean idOk = validateIdNumber(info.getIdNumber());
|
|
||||||
|
|
||||||
log.debug("身份证正面字段校验详情:nameOk={}, genderOk={}, ethnicOk={}, idOk={}",
|
|
||||||
nameOk, genderOk, ethnicOk, idOk);
|
|
||||||
return nameOk && genderOk && ethnicOk && idOk;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean validateBack(IdCardBackInfo info) {
|
|
||||||
// 目前只做存在性校验,后续可以根据业务需要增加日期格式、范围等更严格校验
|
|
||||||
if (info == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
boolean authorityOk = info.getIssuingAuthority() != null && !info.getIssuingAuthority().isEmpty();
|
|
||||||
boolean validFromOk = info.getValidFrom() != null && !info.getValidFrom().isEmpty();
|
|
||||||
boolean validToOk = info.getValidTo() != null && !info.getValidTo().isEmpty();
|
|
||||||
log.info("身份证反面字段校验详情:authorityOk={}, validFromOk={}, validToOk={}",
|
|
||||||
authorityOk, validFromOk, validToOk);
|
|
||||||
return authorityOk && validFromOk && validToOk;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean validate(IdCardInfo info) {
|
|
||||||
if (info == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
boolean frontOk = info.getFront() == null || validateFront(info.getFront());
|
|
||||||
boolean backOk = info.getBack() == null || validateBack(info.getBack());
|
|
||||||
return frontOk && backOk;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 简单的身份证号码格式 + 校验位校验
|
|
||||||
*/
|
|
||||||
public boolean validateIdNumber(String id) {
|
|
||||||
if (id == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String upper = id.toUpperCase();
|
|
||||||
if (!upper.matches("^[1-9]\\d{5}(19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9X]$")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 校验位
|
|
||||||
char[] chars = upper.toCharArray();
|
|
||||||
int[] weight = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};
|
|
||||||
char[] validateCode = {'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'};
|
|
||||||
int sum = 0;
|
|
||||||
for (int i = 0; i < 17; i++) {
|
|
||||||
sum += (chars[i] - '0') * weight[i];
|
|
||||||
}
|
|
||||||
int mod = sum % 11;
|
|
||||||
return validateCode[mod] == chars[17];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -94,10 +94,8 @@ public class OcrCommonDetModelImpl implements OcrCommonDetModel{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<OcrBox> detect(Image image){
|
public List<OcrBox> detect(Image image){
|
||||||
long start = System.nanoTime();
|
|
||||||
List<Image> imageList = Collections.singletonList(image);
|
List<Image> imageList = Collections.singletonList(image);
|
||||||
List<List<OcrBox>> result = batchDetectDJLImage(imageList);
|
List<List<OcrBox>> result = batchDetectDJLImage(imageList);
|
||||||
log.debug("文本检测模型单图调用耗时={}ms, 检测框数量={}", elapsedMillis(start), result.isEmpty() ? 0 : result.get(0).size());
|
|
||||||
return result.get(0);
|
return result.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,19 +192,12 @@ public class OcrCommonDetModelImpl implements OcrCommonDetModel{
|
|||||||
if(!ImageUtils.isAllImageSizeEqual(imageList)){
|
if(!ImageUtils.isAllImageSizeEqual(imageList)){
|
||||||
throw new OcrException("图片尺寸不一致");
|
throw new OcrException("图片尺寸不一致");
|
||||||
}
|
}
|
||||||
long totalStart = System.nanoTime();
|
|
||||||
Predictor<Image, NDList> predictor = null;
|
Predictor<Image, NDList> predictor = null;
|
||||||
try (NDManager manager = NDManager.newBaseManager()) {
|
try (NDManager manager = NDManager.newBaseManager()) {
|
||||||
predictor = detPredictorPool.borrowObject();
|
predictor = detPredictorPool.borrowObject();
|
||||||
List<NDList> result = predictor.batchPredict(imageList);
|
List<NDList> result = predictor.batchPredict(imageList);
|
||||||
result.forEach(ndList -> ndList.attach(manager));
|
result.forEach(ndList -> ndList.attach(manager));
|
||||||
List<List<OcrBox>> boxes = OcrUtils.convertToOcrBox(result);
|
return OcrUtils.convertToOcrBox(result);
|
||||||
int totalBoxes = 0;
|
|
||||||
for (List<OcrBox> boxList : boxes) {
|
|
||||||
totalBoxes += boxList == null ? 0 : boxList.size();
|
|
||||||
}
|
|
||||||
log.debug("文本检测总耗时={}ms, batchSize={}, totalBoxes={}", elapsedMillis(totalStart), imageList.size(), totalBoxes);
|
|
||||||
return boxes;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new OcrException("OCR检测错误", e);
|
throw new OcrException("OCR检测错误", e);
|
||||||
}finally {
|
}finally {
|
||||||
@@ -266,9 +257,5 @@ public class OcrCommonDetModelImpl implements OcrCommonDetModel{
|
|||||||
return fromFactory;
|
return fromFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long elapsedMillis(long startNanos) {
|
|
||||||
return (System.nanoTime() - startNanos) / 1_000_000;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,13 +108,13 @@ public class PPOCRMobileV2ClsModel implements OcrDirectionModel {
|
|||||||
if(Objects.isNull(textDetModel)){
|
if(Objects.isNull(textDetModel)){
|
||||||
throw new OcrException("textDetModel is null");
|
throw new OcrException("textDetModel is null");
|
||||||
}
|
}
|
||||||
|
//检测文本
|
||||||
List<OcrBox> boxeList = textDetModel.detect(image);
|
List<OcrBox> boxeList = textDetModel.detect(image);
|
||||||
if(Objects.isNull(boxeList) || boxeList.isEmpty()){
|
if(Objects.isNull(boxeList) || boxeList.isEmpty()){
|
||||||
throw new OcrException("未检测到文本");
|
throw new OcrException("未检测到文本");
|
||||||
}
|
}
|
||||||
Mat srcMat = ImageUtils.toMat(image);
|
Mat srcMat = ImageUtils.toMat(image);
|
||||||
List<OcrItem> result = detect(boxeList, srcMat);
|
return detect(boxeList, srcMat);
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -274,9 +274,7 @@ public class PPOCRMobileV2ClsModel implements OcrDirectionModel {
|
|||||||
List<Image> imageList = new ArrayList<Image>();
|
List<Image> imageList = new ArrayList<Image>();
|
||||||
List<Boolean> isRotatedList = new ArrayList<Boolean>();
|
List<Boolean> isRotatedList = new ArrayList<Boolean>();
|
||||||
int index = 0;
|
int index = 0;
|
||||||
long totalStart = System.nanoTime();
|
|
||||||
try (NDManager manager = model.getNDManager().newSubManager()){
|
try (NDManager manager = model.getNDManager().newSubManager()){
|
||||||
long prepareStart = System.nanoTime();
|
|
||||||
for(int i = 0; i < srcMatList.size(); i++){
|
for(int i = 0; i < srcMatList.size(); i++){
|
||||||
for (int j = 0; j < boxList.get(i).size(); j++){
|
for (int j = 0; j < boxList.get(i).size(); j++){
|
||||||
//透视变换及裁剪
|
//透视变换及裁剪
|
||||||
@@ -294,11 +292,8 @@ public class PPOCRMobileV2ClsModel implements OcrDirectionModel {
|
|||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.debug("方向模型裁剪预处理耗时={}ms, batchSize={}, textBlocks={}", elapsedMillis(prepareStart), srcMatList.size(), imageList.size());
|
|
||||||
List<List<OcrItem>> result = new ArrayList<>();
|
List<List<OcrItem>> result = new ArrayList<>();
|
||||||
long predictStart = System.nanoTime();
|
|
||||||
List<DirectionInfo> directionInfos = batchDetect(imageList);
|
List<DirectionInfo> directionInfos = batchDetect(imageList);
|
||||||
log.debug("方向分类模型调用耗时={}ms, textBlocks={}", elapsedMillis(predictStart), imageList.size());
|
|
||||||
//释放
|
//释放
|
||||||
imageList.forEach(image -> ImageUtils.releaseOpenCVMat(image));
|
imageList.forEach(image -> ImageUtils.releaseOpenCVMat(image));
|
||||||
if(CollectionUtils.isEmpty(directionInfos)){
|
if(CollectionUtils.isEmpty(directionInfos)){
|
||||||
@@ -332,7 +327,6 @@ public class PPOCRMobileV2ClsModel implements OcrDirectionModel {
|
|||||||
}
|
}
|
||||||
result.add(ocrItemList);
|
result.add(ocrItemList);
|
||||||
}
|
}
|
||||||
log.debug("方向模型总耗时={}ms, batchSize={}, textBlocks={}", elapsedMillis(totalStart), srcMatList.size(), index);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,8 +335,7 @@ public class PPOCRMobileV2ClsModel implements OcrDirectionModel {
|
|||||||
Predictor<Image, DirectionInfo> predictor = null;
|
Predictor<Image, DirectionInfo> predictor = null;
|
||||||
try {
|
try {
|
||||||
predictor = predictorPool.borrowObject();
|
predictor = predictorPool.borrowObject();
|
||||||
List<DirectionInfo> result = predictor.batchPredict(imageList);
|
return predictor.batchPredict(imageList);
|
||||||
return result;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new OcrException("OCR检测错误", e);
|
throw new OcrException("OCR检测错误", e);
|
||||||
}finally {
|
}finally {
|
||||||
@@ -406,8 +399,4 @@ public class PPOCRMobileV2ClsModel implements OcrDirectionModel {
|
|||||||
public boolean isFromFactory() {
|
public boolean isFromFactory() {
|
||||||
return fromFactory;
|
return fromFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long elapsedMillis(long startNanos) {
|
|
||||||
return (System.nanoTime() - startNanos) / 1_000_000;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,19 +61,6 @@ public interface OcrCommonRecModel extends AutoCloseable{
|
|||||||
throw new UnsupportedOperationException("默认不支持该功能");
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 基于已有文本检测框执行文本识别。
|
|
||||||
* 适合调用方已经完成文本检测,想避免重复检测的场景。
|
|
||||||
*
|
|
||||||
* @param image 原图
|
|
||||||
* @param boxList 已有文本检测框
|
|
||||||
* @param options 识别选项
|
|
||||||
* @return OCR 结果
|
|
||||||
*/
|
|
||||||
default OcrInfo recognize(Image image, List<OcrBox> boxList, OcrRecOptions options) {
|
|
||||||
throw new UnsupportedOperationException("默认不支持该功能");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文本检测
|
* 文本检测
|
||||||
@@ -154,18 +141,6 @@ public interface OcrCommonRecModel extends AutoCloseable{
|
|||||||
throw new UnsupportedOperationException("默认不支持该功能");
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 基于已有文本检测框执行批量文本识别。
|
|
||||||
*
|
|
||||||
* @param imageList 原图列表
|
|
||||||
* @param boxList 每张图对应的文本检测框列表
|
|
||||||
* @param options 识别选项
|
|
||||||
* @return OCR 结果
|
|
||||||
*/
|
|
||||||
default List<OcrInfo> batchRecognizeDJLImage(List<Image> imageList, List<List<OcrBox>> boxList, OcrRecOptions options) {
|
|
||||||
throw new UnsupportedOperationException("默认不支持该功能");
|
|
||||||
}
|
|
||||||
|
|
||||||
default GenericObjectPool<Predictor<Image, String>> getPool() {
|
default GenericObjectPool<Predictor<Image, String>> getPool() {
|
||||||
throw new UnsupportedOperationException("默认不支持该功能");
|
throw new UnsupportedOperationException("默认不支持该功能");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ import java.util.stream.Collectors;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
||||||
|
|
||||||
private static final int REC_CHUNK_SIZE = 64;
|
|
||||||
|
|
||||||
private GenericObjectPool<Predictor<Image, String>> recPredictorPool;
|
private GenericObjectPool<Predictor<Image, String>> recPredictorPool;
|
||||||
|
|
||||||
private OcrRecModelConfig config;
|
private OcrRecModelConfig config;
|
||||||
@@ -113,27 +111,10 @@ public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public OcrInfo recognize(Image image, OcrRecOptions options) {
|
public OcrInfo recognize(Image image, OcrRecOptions options) {
|
||||||
long start = System.nanoTime();
|
|
||||||
List<OcrInfo> result = batchRecognizeDJLImage(Collections.singletonList(image), options);
|
List<OcrInfo> result = batchRecognizeDJLImage(Collections.singletonList(image), options);
|
||||||
if (CollectionUtils.isEmpty(result)) {
|
if (CollectionUtils.isEmpty(result)) {
|
||||||
throw new OcrException("OCR识别结果为空");
|
throw new OcrException("OCR识别结果为空");
|
||||||
}
|
}
|
||||||
log.debug("OCR识别单图总耗时={}ms", elapsedMillis(start));
|
|
||||||
return result.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OcrInfo recognize(Image image, List<OcrBox> boxList, OcrRecOptions options) {
|
|
||||||
long start = System.nanoTime();
|
|
||||||
List<OcrInfo> result = batchRecognizeDJLImage(
|
|
||||||
Collections.singletonList(image),
|
|
||||||
Collections.singletonList(boxList),
|
|
||||||
options
|
|
||||||
);
|
|
||||||
if (CollectionUtils.isEmpty(result)) {
|
|
||||||
throw new OcrException("OCR识别结果为空");
|
|
||||||
}
|
|
||||||
log.debug("OCR识别单图总耗时={}ms", elapsedMillis(start));
|
|
||||||
return result.get(0);
|
return result.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,8 +212,10 @@ public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
|||||||
//分行判断
|
//分行判断
|
||||||
for (int i = 1; i < initList.size(); i++) {
|
for (int i = 1; i < initList.size(); i++) {
|
||||||
RotatedBoxCompX tmpBox = new RotatedBoxCompX(initList.get(i).getBox(), initList.get(i).getText());
|
RotatedBoxCompX tmpBox = new RotatedBoxCompX(initList.get(i).getBox(), initList.get(i).getText());
|
||||||
boolean isSameRow = OcrUtils.isSameRow(firstBox.getBox(), tmpBox.getBox());
|
float y1 = firstBox.getBox().toFloatArray()[1];
|
||||||
if (isSameRow) {
|
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);
|
line.add(tmpBox);
|
||||||
} else { // 换行 - Line break
|
} else { // 换行 - Line break
|
||||||
firstBox = tmpBox;
|
firstBox = tmpBox;
|
||||||
@@ -363,11 +346,6 @@ public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<OcrInfo> batchRecognizeDJLImage(List<Image> imageList, OcrRecOptions options) {
|
public List<OcrInfo> batchRecognizeDJLImage(List<Image> imageList, OcrRecOptions options) {
|
||||||
return batchRecognizeDJLImage(imageList, null, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<OcrInfo> batchRecognizeDJLImage(List<Image> imageList, List<List<OcrBox>> boxeList, OcrRecOptions options) {
|
|
||||||
if (Objects.isNull(textDetModel)) {
|
if (Objects.isNull(textDetModel)) {
|
||||||
throw new OcrException("textDetModel is null");
|
throw new OcrException("textDetModel is null");
|
||||||
}
|
}
|
||||||
@@ -378,82 +356,64 @@ public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
|||||||
if (CollectionUtils.isEmpty(imageList)) {
|
if (CollectionUtils.isEmpty(imageList)) {
|
||||||
throw new OcrException("imageList is empty");
|
throw new OcrException("imageList is empty");
|
||||||
}
|
}
|
||||||
long totalStart = System.nanoTime();
|
//检测文本
|
||||||
List<List<OcrBox>> effectiveBoxList = boxeList;
|
List<List<OcrBox>> boxeList = textDetModel.batchDetectDJLImage(imageList);
|
||||||
if (CollectionUtils.isEmpty(effectiveBoxList)) {
|
if (CollectionUtils.isEmpty(boxeList) || boxeList.size() != imageList.size()) {
|
||||||
effectiveBoxList = textDetModel.batchDetectDJLImage(imageList);
|
|
||||||
}
|
|
||||||
if (CollectionUtils.isEmpty(effectiveBoxList) || effectiveBoxList.size() != imageList.size()) {
|
|
||||||
throw new OcrException("未检测到文本");
|
throw new OcrException("未检测到文本");
|
||||||
}
|
}
|
||||||
Predictor<Image, String> predictor = null;
|
Predictor<Image, String> predictor = null;
|
||||||
List<OcrInfo> ocrInfoList = new ArrayList<OcrInfo>();
|
List<OcrInfo> ocrInfoList = new ArrayList<OcrInfo>();
|
||||||
try (NDManager manager = NDManager.newBaseManager()) {
|
try (NDManager manager = NDManager.newBaseManager()) {
|
||||||
predictor = recPredictorPool.borrowObject();
|
predictor = recPredictorPool.borrowObject();
|
||||||
|
List<Image> allImageAlignList = new ArrayList<Image>();
|
||||||
//检测方向
|
//检测方向
|
||||||
if (ocrRecOptions.isEnableDirectionCorrect()) {
|
if (ocrRecOptions.isEnableDirectionCorrect()) {
|
||||||
if (Objects.isNull(directionModel)) {
|
if (Objects.isNull(directionModel)) {
|
||||||
throw new OcrException("请配置方向模型");
|
throw new OcrException("请配置方向模型");
|
||||||
}
|
}
|
||||||
long directionStart = System.nanoTime();
|
List<Mat> matList = imageList.stream()
|
||||||
List<Mat> matList = new ArrayList<>(imageList.size());
|
.map(image -> ImageUtils.toMat(image))
|
||||||
try {
|
.collect(Collectors.toList());
|
||||||
for (Image image : imageList) {
|
List<List<OcrItem>> ocrItemList = directionModel.batchDetect(boxeList, matList);
|
||||||
matList.add(ImageUtils.toMat(image));
|
if (CollectionUtils.isEmpty(ocrItemList) || ocrItemList.size() != imageList.size()) {
|
||||||
}
|
throw new OcrException("方向检测失败");
|
||||||
List<List<OcrItem>> ocrItemList = directionModel.batchDetect(effectiveBoxList, matList);
|
}
|
||||||
log.debug("OCR流程-文本方向分类耗时={}ms, batchSize={}", elapsedMillis(directionStart), imageList.size());
|
allImageAlignList = new ArrayList<Image>();
|
||||||
if (CollectionUtils.isEmpty(ocrItemList) || ocrItemList.size() != imageList.size()) {
|
for (int i = 0; i < ocrItemList.size(); i++) {
|
||||||
throw new OcrException("方向检测失败");
|
Mat srcMat = ImageUtils.toMat(imageList.get(i));
|
||||||
}
|
List<Image> imageAlignList = batchAlignWithDirection(ocrItemList.get(i), srcMat, manager);
|
||||||
long alignStart = System.nanoTime();
|
// for(int j = 0; j < imageAlignList.size(); j++){
|
||||||
List<String> textList = new ArrayList<>();
|
// ImageUtils.saveImage(imageAlignList.get(j),"dir-"+i+"-"+j+".png","/Users/xxx/Downloads/testing33");
|
||||||
List<Image> chunkImages = new ArrayList<>(REC_CHUNK_SIZE);
|
// }
|
||||||
for (int i = 0; i < ocrItemList.size(); i++) {
|
allImageAlignList.addAll(imageAlignList);
|
||||||
Mat srcMat = matList.get(i);
|
|
||||||
List<Image> imageAlignList = batchAlignWithDirection(ocrItemList.get(i), srcMat, manager);
|
|
||||||
for (Image alignImage : imageAlignList) {
|
|
||||||
chunkImages.add(alignImage);
|
|
||||||
if (chunkImages.size() >= REC_CHUNK_SIZE) {
|
|
||||||
textList.addAll(batchRecognizeChunk(predictor, chunkImages));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.debug("OCR流程-方向矫正裁剪耗时={}ms, textBlocks={}", elapsedMillis(alignStart), textList.size() + chunkImages.size());
|
|
||||||
long recStart = System.nanoTime();
|
|
||||||
if (!chunkImages.isEmpty()) {
|
|
||||||
textList.addAll(batchRecognizeChunk(predictor, chunkImages));
|
|
||||||
}
|
|
||||||
log.debug("OCR流程-识别模型调用耗时={}ms, textBlocks={}", elapsedMillis(recStart), textList.size());
|
|
||||||
return buildOcrInfoList(effectiveBoxList, ocrRecOptions, manager, textList, imageList.size(), totalStart);
|
|
||||||
} finally {
|
|
||||||
releaseTemporaryMats(imageList, matList);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
List<String> textList = new ArrayList<>();
|
for (int i = 0; i < boxeList.size(); i++) {
|
||||||
List<Image> chunkImages = new ArrayList<>(REC_CHUNK_SIZE);
|
Mat srcMat = ImageUtils.toMat(imageList.get(i));
|
||||||
for (int i = 0; i < effectiveBoxList.size(); i++) {
|
List<Image> imageAlignList = batchAlign(boxeList.get(i), srcMat, manager);
|
||||||
Mat srcMat = null;
|
// for(int j = 0; j < imageAlignList.size(); j++){
|
||||||
try {
|
// ImageUtils.saveImage(imageAlignList.get(j),i+"-"+j+".png","/Users/wenjie/Downloads/testing33");
|
||||||
srcMat = ImageUtils.toMat(imageList.get(i));
|
// }
|
||||||
List<Image> imageAlignList = batchAlign(effectiveBoxList.get(i), srcMat, manager);
|
allImageAlignList.addAll(imageAlignList);
|
||||||
for (Image alignImage : imageAlignList) {
|
|
||||||
chunkImages.add(alignImage);
|
|
||||||
if (chunkImages.size() >= REC_CHUNK_SIZE) {
|
|
||||||
textList.addAll(batchRecognizeChunk(predictor, chunkImages));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
releaseTemporaryMat(imageList.get(i), srcMat);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
long recStart = System.nanoTime();
|
|
||||||
if (!chunkImages.isEmpty()) {
|
|
||||||
textList.addAll(batchRecognizeChunk(predictor, chunkImages));
|
|
||||||
}
|
|
||||||
log.debug("OCR流程-识别模型调用耗时={}ms, textBlocks={}", elapsedMillis(recStart), textList.size());
|
|
||||||
return buildOcrInfoList(effectiveBoxList, ocrRecOptions, manager, textList, imageList.size(), totalStart);
|
|
||||||
}
|
}
|
||||||
|
List<String> textList = batchRecognize(allImageAlignList);
|
||||||
|
int textIndex = 0;
|
||||||
|
for (int i = 0; i < boxeList.size(); i++) {
|
||||||
|
List<RotatedBox> rotatedBoxes = new ArrayList<>();
|
||||||
|
for (int j = 0; j < boxeList.get(i).size(); j++) {
|
||||||
|
if (textIndex >= textList.size()) {
|
||||||
|
throw new OcrException("识别失败: 第" + i + "张图片, 第" + j + "个文本块,未识别到文本");
|
||||||
|
}
|
||||||
|
OcrBox box = boxeList.get(i).get(j);
|
||||||
|
NDArray pointsArray = manager.create(box.toFloatArray());
|
||||||
|
rotatedBoxes.add(new RotatedBox(pointsArray, textList.get(textIndex)));
|
||||||
|
textIndex++;
|
||||||
|
}
|
||||||
|
OcrInfo ocrInfo = postProcessOcrResult(rotatedBoxes, ocrRecOptions);
|
||||||
|
ocrInfoList.add(ocrInfo);
|
||||||
|
}
|
||||||
|
return ocrInfoList;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new OcrException("OCR检测错误", e);
|
throw new OcrException("OCR检测错误", e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -472,41 +432,28 @@ public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<OcrInfo> buildOcrInfoList(List<List<OcrBox>> effectiveBoxList,
|
private List<String> batchRecognize(List<Image> imageAlignList) {
|
||||||
OcrRecOptions ocrRecOptions,
|
Predictor<Image, String> predictor = null;
|
||||||
NDManager manager,
|
|
||||||
List<String> textList,
|
|
||||||
int batchSize,
|
|
||||||
long totalStart) {
|
|
||||||
int textIndex = 0;
|
|
||||||
List<OcrInfo> ocrInfoList = new ArrayList<>();
|
|
||||||
for (int i = 0; i < effectiveBoxList.size(); i++) {
|
|
||||||
List<RotatedBox> rotatedBoxes = new ArrayList<>();
|
|
||||||
for (int j = 0; j < effectiveBoxList.get(i).size(); j++) {
|
|
||||||
if (textIndex >= textList.size()) {
|
|
||||||
throw new OcrException("识别失败: 第" + i + "张图片, 第" + j + "个文本块,未识别到文本");
|
|
||||||
}
|
|
||||||
OcrBox box = effectiveBoxList.get(i).get(j);
|
|
||||||
NDArray pointsArray = manager.create(box.toFloatArray());
|
|
||||||
rotatedBoxes.add(new RotatedBox(pointsArray, textList.get(textIndex)));
|
|
||||||
textIndex++;
|
|
||||||
}
|
|
||||||
OcrInfo ocrInfo = postProcessOcrResult(rotatedBoxes, ocrRecOptions);
|
|
||||||
ocrInfoList.add(ocrInfo);
|
|
||||||
}
|
|
||||||
log.debug("OCR流程总耗时={}ms, batchSize={}", elapsedMillis(totalStart), batchSize);
|
|
||||||
return ocrInfoList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> batchRecognizeChunk(Predictor<Image, String> predictor, List<Image> imageAlignList) {
|
|
||||||
try {
|
try {
|
||||||
|
predictor = recPredictorPool.borrowObject();
|
||||||
List<String> textList = predictor.batchPredict(imageAlignList);
|
List<String> textList = predictor.batchPredict(imageAlignList);
|
||||||
|
imageAlignList.forEach(subImg -> ImageUtils.releaseOpenCVMat(subImg));
|
||||||
return textList;
|
return textList;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new OcrException("OCR检测错误", e);
|
throw new OcrException("OCR检测错误", e);
|
||||||
} finally {
|
} finally {
|
||||||
imageAlignList.forEach(ImageUtils::releaseOpenCVMat);
|
if (predictor != null) {
|
||||||
imageAlignList.clear();
|
try {
|
||||||
|
recPredictorPool.returnObject(predictor); //归还
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("归还Predictor失败", e);
|
||||||
|
try {
|
||||||
|
predictor.close(); // 归还失败才销毁
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("关闭Predictor失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,24 +525,4 @@ public class OcrCommonRecModelImpl implements OcrCommonRecModel {
|
|||||||
public boolean isFromFactory() {
|
public boolean isFromFactory() {
|
||||||
return fromFactory;
|
return fromFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long elapsedMillis(long startNanos) {
|
|
||||||
return (System.nanoTime() - startNanos) / 1_000_000;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void releaseTemporaryMats(List<Image> imageList, List<Mat> matList) {
|
|
||||||
int size = Math.min(imageList.size(), matList.size());
|
|
||||||
for (int i = 0; i < size; i++) {
|
|
||||||
releaseTemporaryMat(imageList.get(i), matList.get(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void releaseTemporaryMat(Image image, Mat mat) {
|
|
||||||
if (mat == null || image == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!(image.getWrappedImage() instanceof Mat)) {
|
|
||||||
mat.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,278 +0,0 @@
|
|||||||
package cn.smartjavaai.ocr.utils;
|
|
||||||
|
|
||||||
import cn.smartjavaai.common.entity.Point;
|
|
||||||
import cn.smartjavaai.ocr.entity.OcrBox;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author dwj
|
|
||||||
* @date 2026/1/11
|
|
||||||
*/
|
|
||||||
public class BoxUtils {
|
|
||||||
|
|
||||||
|
|
||||||
public enum Direction {
|
|
||||||
UP, DOWN, LEFT, RIGHT
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 寻找指定方向上距离最近的框
|
|
||||||
*
|
|
||||||
* @param anchor 锚点框,作为搜索的起始参考框
|
|
||||||
* @param boxList 候选框列表,在其中搜索目标框
|
|
||||||
* @param direction 搜索方向,可选 UP(上)、DOWN(下)、LEFT(左)、RIGHT(右)
|
|
||||||
* @return 找到的最近的 OcrBox,如果找不到符合条件的框则返回 null
|
|
||||||
*
|
|
||||||
* @apiNote
|
|
||||||
* - 该方法会在指定方向上寻找与锚点框在同一行或同一列的最近邻框
|
|
||||||
* - 水平方向(LEFT/RIGHT):要求候选框与锚点框在 Y 轴上有重叠(同一行)
|
|
||||||
* - 垂直方向(UP/DOWN):要求候选框与锚点框在 X 轴上有重叠(同一列)
|
|
||||||
* - 使用欧几里得距离计算中心点之间的距离
|
|
||||||
*/
|
|
||||||
public static OcrBox findNearestBox(OcrBox anchor, List<OcrBox> boxList, Direction direction) {
|
|
||||||
OcrBox nearest = null;
|
|
||||||
double bestScore = Double.MAX_VALUE;
|
|
||||||
AxisSystem axisSystem = buildAxisSystem(anchor);
|
|
||||||
|
|
||||||
for (OcrBox target : boxList) {
|
|
||||||
if (target == anchor) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
CandidateMetrics metrics = evaluateCandidate(anchor, target, direction, axisSystem);
|
|
||||||
if (metrics == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (metrics.score < bestScore) {
|
|
||||||
bestScore = metrics.score;
|
|
||||||
nearest = target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nearest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 寻找指定方向上距离最近的多个框(按距离升序返回)
|
|
||||||
*
|
|
||||||
* @param anchor 锚点框
|
|
||||||
* @param boxList 候选框列表
|
|
||||||
* @param direction 搜索方向
|
|
||||||
* @param limit 返回的最大数量(<=0 时返回空列表)
|
|
||||||
* @return 按距离由近到远排序的 OcrBox 列表
|
|
||||||
*/
|
|
||||||
public static List<OcrBox> findNearestBoxes(OcrBox anchor, List<OcrBox> boxList, Direction direction, int limit) {
|
|
||||||
if (anchor == null || boxList == null || boxList.isEmpty() || limit <= 0) {
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
AxisSystem axisSystem = buildAxisSystem(anchor);
|
|
||||||
List<Neighbor> candidates = new ArrayList<>();
|
|
||||||
|
|
||||||
for (OcrBox target : boxList) {
|
|
||||||
if (target == anchor) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
CandidateMetrics metrics = evaluateCandidate(anchor, target, direction, axisSystem);
|
|
||||||
if (metrics == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
candidates.add(new Neighbor(target, metrics.score));
|
|
||||||
}
|
|
||||||
|
|
||||||
candidates.sort(Comparator.comparingDouble(n -> n.distance));
|
|
||||||
|
|
||||||
List<OcrBox> result = new ArrayList<>();
|
|
||||||
int size = Math.min(limit, candidates.size());
|
|
||||||
for (int i = 0; i < size; i++) {
|
|
||||||
result.add(candidates.get(i).box);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 内部使用的邻居结构体,存储框和距离
|
|
||||||
*/
|
|
||||||
private static class Neighbor {
|
|
||||||
private final OcrBox box;
|
|
||||||
private final double distance;
|
|
||||||
|
|
||||||
private Neighbor(OcrBox box, double distance) {
|
|
||||||
this.box = box;
|
|
||||||
this.distance = distance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CandidateMetrics evaluateCandidate(OcrBox anchor, OcrBox target, Direction direction, AxisSystem axisSystem) {
|
|
||||||
double mainAxisX = isHorizontalDirection(direction) ? axisSystem.horizontalAxisX : axisSystem.verticalAxisX;
|
|
||||||
double mainAxisY = isHorizontalDirection(direction) ? axisSystem.horizontalAxisY : axisSystem.verticalAxisY;
|
|
||||||
double crossAxisX = isHorizontalDirection(direction) ? axisSystem.verticalAxisX : axisSystem.horizontalAxisX;
|
|
||||||
double crossAxisY = isHorizontalDirection(direction) ? axisSystem.verticalAxisY : axisSystem.horizontalAxisY;
|
|
||||||
|
|
||||||
Projection anchorMain = projectBox(anchor, mainAxisX, mainAxisY);
|
|
||||||
Projection anchorCross = projectBox(anchor, crossAxisX, crossAxisY);
|
|
||||||
Projection targetMain = projectBox(target, mainAxisX, mainAxisY);
|
|
||||||
Projection targetCross = projectBox(target, crossAxisX, crossAxisY);
|
|
||||||
Point anchorCenter = getCenter(anchor);
|
|
||||||
Point targetCenter = getCenter(target);
|
|
||||||
double centerMainDelta = projectPointDelta(anchorCenter, targetCenter, mainAxisX, mainAxisY);
|
|
||||||
|
|
||||||
double mainGap;
|
|
||||||
switch (direction) {
|
|
||||||
case RIGHT:
|
|
||||||
case DOWN:
|
|
||||||
mainGap = targetMain.min - anchorMain.max;
|
|
||||||
if (centerMainDelta <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case LEFT:
|
|
||||||
case UP:
|
|
||||||
mainGap = anchorMain.min - targetMain.max;
|
|
||||||
if (centerMainDelta >= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
double anchorMainSize = Math.max(1.0, anchorMain.max - anchorMain.min);
|
|
||||||
double targetMainSize = Math.max(1.0, targetMain.max - targetMain.min);
|
|
||||||
double allowedBacktrack = Math.min(anchorMainSize, targetMainSize) * 0.35;
|
|
||||||
if (mainGap < -allowedBacktrack) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
double overlap = Math.max(0.0, Math.min(anchorCross.max, targetCross.max) - Math.max(anchorCross.min, targetCross.min));
|
|
||||||
double minCrossSize = Math.max(1.0, Math.min(anchorCross.max - anchorCross.min, targetCross.max - targetCross.min));
|
|
||||||
double overlapRatio = overlap / minCrossSize;
|
|
||||||
|
|
||||||
double anchorCrossCenter = (anchorCross.min + anchorCross.max) / 2.0;
|
|
||||||
double targetCrossCenter = (targetCross.min + targetCross.max) / 2.0;
|
|
||||||
double crossCenterDistance = Math.abs(anchorCrossCenter - targetCrossCenter);
|
|
||||||
double crossTolerance = Math.max(anchorCross.max - anchorCross.min, targetCross.max - targetCross.min) * 0.6;
|
|
||||||
|
|
||||||
if (overlapRatio < 0.2 && crossCenterDistance > crossTolerance) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
double score = Math.max(0.0, mainGap) * 10.0 + crossCenterDistance + Math.abs(centerMainDelta) * 0.01;
|
|
||||||
if (overlapRatio < 0.2) {
|
|
||||||
score += (0.2 - overlapRatio) * 100.0;
|
|
||||||
}
|
|
||||||
return new CandidateMetrics(score);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isHorizontalDirection(Direction direction) {
|
|
||||||
return direction == Direction.LEFT || direction == Direction.RIGHT;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Point getCenter(OcrBox box) {
|
|
||||||
float cx = (float) (box.getTopLeft().getX() + box.getTopRight().getX() + box.getBottomRight().getX() + box.getBottomLeft().getX()) / 4;
|
|
||||||
float cy = (float) (box.getTopLeft().getY() + box.getTopRight().getY() + box.getBottomRight().getY() + box.getBottomLeft().getY()) / 4;
|
|
||||||
return new Point(cx, cy);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AxisSystem buildAxisSystem(OcrBox anchor) {
|
|
||||||
Point topLeft = anchor.getTopLeft();
|
|
||||||
Point topRight = anchor.getTopRight();
|
|
||||||
Point bottomLeft = anchor.getBottomLeft();
|
|
||||||
|
|
||||||
double horizontalX = topRight.getX() - topLeft.getX();
|
|
||||||
double horizontalY = topRight.getY() - topLeft.getY();
|
|
||||||
double verticalX = bottomLeft.getX() - topLeft.getX();
|
|
||||||
double verticalY = bottomLeft.getY() - topLeft.getY();
|
|
||||||
|
|
||||||
double horizontalNorm = Math.hypot(horizontalX, horizontalY);
|
|
||||||
double verticalNorm = Math.hypot(verticalX, verticalY);
|
|
||||||
|
|
||||||
if (horizontalNorm < 1e-6) {
|
|
||||||
horizontalX = 1.0;
|
|
||||||
horizontalY = 0.0;
|
|
||||||
horizontalNorm = 1.0;
|
|
||||||
}
|
|
||||||
if (verticalNorm < 1e-6) {
|
|
||||||
verticalX = 0.0;
|
|
||||||
verticalY = 1.0;
|
|
||||||
verticalNorm = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AxisSystem(
|
|
||||||
horizontalX / horizontalNorm,
|
|
||||||
horizontalY / horizontalNorm,
|
|
||||||
verticalX / verticalNorm,
|
|
||||||
verticalY / verticalNorm
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Projection projectBox(OcrBox box, double axisX, double axisY) {
|
|
||||||
double[] values = new double[]{
|
|
||||||
dot(box.getTopLeft(), axisX, axisY),
|
|
||||||
dot(box.getTopRight(), axisX, axisY),
|
|
||||||
dot(box.getBottomRight(), axisX, axisY),
|
|
||||||
dot(box.getBottomLeft(), axisX, axisY)
|
|
||||||
};
|
|
||||||
double min = values[0];
|
|
||||||
double max = values[0];
|
|
||||||
for (int i = 1; i < values.length; i++) {
|
|
||||||
min = Math.min(min, values[i]);
|
|
||||||
max = Math.max(max, values[i]);
|
|
||||||
}
|
|
||||||
return new Projection(min, max);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double dot(Point point, double axisX, double axisY) {
|
|
||||||
return point.getX() * axisX + point.getY() * axisY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double projectPointDelta(Point from, Point to, double axisX, double axisY) {
|
|
||||||
return (to.getX() - from.getX()) * axisX + (to.getY() - from.getY()) * axisY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double min(double a, double b, double c, double d) {
|
|
||||||
return Math.min(Math.min(a, b), Math.min(c, d));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double max(double a, double b, double c, double d) {
|
|
||||||
return Math.max(Math.max(a, b), Math.max(c, d));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class AxisSystem {
|
|
||||||
private final double horizontalAxisX;
|
|
||||||
private final double horizontalAxisY;
|
|
||||||
private final double verticalAxisX;
|
|
||||||
private final double verticalAxisY;
|
|
||||||
|
|
||||||
private AxisSystem(double horizontalAxisX, double horizontalAxisY, double verticalAxisX, double verticalAxisY) {
|
|
||||||
this.horizontalAxisX = horizontalAxisX;
|
|
||||||
this.horizontalAxisY = horizontalAxisY;
|
|
||||||
this.verticalAxisX = verticalAxisX;
|
|
||||||
this.verticalAxisY = verticalAxisY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Projection {
|
|
||||||
private final double min;
|
|
||||||
private final double max;
|
|
||||||
|
|
||||||
private Projection(double min, double max) {
|
|
||||||
this.min = min;
|
|
||||||
this.max = max;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class CandidateMetrics {
|
|
||||||
private final double score;
|
|
||||||
|
|
||||||
private CandidateMetrics(double score) {
|
|
||||||
this.score = score;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ import ai.djl.modality.cv.util.NDImageUtils;
|
|||||||
import ai.djl.ndarray.NDArray;
|
import ai.djl.ndarray.NDArray;
|
||||||
import ai.djl.ndarray.NDList;
|
import ai.djl.ndarray.NDList;
|
||||||
import ai.djl.ndarray.NDManager;
|
import ai.djl.ndarray.NDManager;
|
||||||
import ai.djl.ndarray.types.Shape;
|
|
||||||
import ai.djl.opencv.OpenCVImageFactory;
|
import ai.djl.opencv.OpenCVImageFactory;
|
||||||
import cn.smartjavaai.common.cv.SmartImageFactory;
|
import cn.smartjavaai.common.cv.SmartImageFactory;
|
||||||
import cn.smartjavaai.common.entity.*;
|
import cn.smartjavaai.common.entity.*;
|
||||||
@@ -413,42 +412,4 @@ public class OcrUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static boolean isSameRow(NDArray box1, NDArray box2) {
|
|
||||||
// 1. 确保 box 是 (4, 2) 的形状
|
|
||||||
NDArray b1 = formatBox(box1);
|
|
||||||
NDArray b2 = formatBox(box2);
|
|
||||||
|
|
||||||
// 2. 获取 Y 坐标列(索引为 1 的列)
|
|
||||||
NDArray y1 = b1.get(":, 1");
|
|
||||||
NDArray y2 = b2.get(":, 1");
|
|
||||||
|
|
||||||
float yMin1 = y1.min().getFloat();
|
|
||||||
float yMax1 = y1.max().getFloat();
|
|
||||||
float yMin2 = y2.min().getFloat();
|
|
||||||
float yMax2 = y2.max().getFloat();
|
|
||||||
|
|
||||||
// 3. 计算重叠高度
|
|
||||||
float overlapHeight = Math.min(yMax1, yMax2) - Math.max(yMin1, yMin2);
|
|
||||||
|
|
||||||
if (overlapHeight <= 0) return false;
|
|
||||||
|
|
||||||
// 4. 计算各自高度
|
|
||||||
float h1 = yMax1 - yMin1;
|
|
||||||
float h2 = yMax2 - yMin2;
|
|
||||||
|
|
||||||
// 5. 判定标准
|
|
||||||
return overlapHeight > (Math.min(h1, h2) * 0.5f);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 辅助方法:将 1D 的 8个元素 转换为 2D 的 (4, 2)
|
|
||||||
*/
|
|
||||||
private static NDArray formatBox(NDArray box) {
|
|
||||||
if (box.getShape().dimension() == 1) {
|
|
||||||
// 如果是 [x0, y0, x1, y1...] 这种 8 个元素的 1D 阵,转为 (4, 2)
|
|
||||||
return box.reshape(new Shape(4, 2));
|
|
||||||
}
|
|
||||||
return box;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
3
pom.xml
3
pom.xml
@@ -7,7 +7,7 @@
|
|||||||
<name>SmartJavaAI</name>
|
<name>SmartJavaAI</name>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-parent</artifactId>
|
<artifactId>smartjavaai-parent</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<description>SmartJavaAI</description>
|
<description>SmartJavaAI</description>
|
||||||
<modules>
|
<modules>
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
<module>ocr</module>
|
<module>ocr</module>
|
||||||
<module>bom</module>
|
<module>bom</module>
|
||||||
<module>speech</module>
|
<module>speech</module>
|
||||||
|
<!-- <module>face-pro</module>-->
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-parent</artifactId>
|
<artifactId>smartjavaai-parent</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>speech</artifactId>
|
<artifactId>speech</artifactId>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
<name>speech</name>
|
<name>speech</name>
|
||||||
<description>SmartJavaAI</description>
|
<description>SmartJavaAI</description>
|
||||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
<name>translate</name>
|
<name>translate</name>
|
||||||
<description>SmartJavaAI</description>
|
<description>SmartJavaAI</description>
|
||||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.smartjavaai</groupId>
|
<groupId>cn.smartjavaai</groupId>
|
||||||
<artifactId>smartjavaai-parent</artifactId>
|
<artifactId>smartjavaai-parent</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>vision</artifactId>
|
<artifactId>vision</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.1.1</version>
|
||||||
<name>vision</name>
|
<name>vision</name>
|
||||||
<description>SmartJavaAI</description>
|
<description>SmartJavaAI</description>
|
||||||
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
<url>https://github.com/geekwenjie/SmartJavaAI</url>
|
||||||
|
|||||||
Reference in New Issue
Block a user