Officia.OCR — 图片与扫描件识别
能力摘要(AI 友好):
OfficiaOcr把图片识别成文字(recognize返回纯文本、analyze返回逐行文本 + 坐标 + 置信度);ScannedPdfConverter把扫描件 PDF 转成可编辑 DOCX。中英两档权重随包内置(int8,合计约 11.7 MB),纯 JDK 自研推理内核,不依赖 Tesseract / ONNX Runtime / OpenCV。
把位图变成文字:发票号、证件字段、整页扫描书稿。识别内核与权重都是自研的, 引入依赖即可用——不需要在服务器上装任何 OCR 软件,也不需要联网调用外部识别服务。
引入
<dependency>
<groupId>plus.ruoyi</groupId>
<artifactId>officia-all</artifactId>
<version>1.0.0</version>
</dependency>这是唯一需要引的依赖:Maven Central 上只发布
officia-all这一个构件, 它是含全部能力模块与授权客户端的单一 jar,且无任何第三方传递依赖。
图片 → 文字
import plus.ruoyi.officia.ocr.OfficiaOcr;
import plus.ruoyi.officia.ocr.OcrOptions;
import plus.ruoyi.officia.ocr.recog.BuiltinModels.Language;
// 纯文本
String text = OfficiaOcr.recognize(imageBytes,
OcrOptions.defaults().setLanguage(Language.CHINESE));
// 也接受 InputStream / File
String t2 = OfficiaOcr.recognize(new File("scan.png"), opts);富结果:逐行文本 + 坐标 + 置信度
要定位「哪一行识别得不好」、或者要按位置做后续处理时用 analyze:
import plus.ruoyi.officia.ocr.result.OcrResult;
import plus.ruoyi.officia.ocr.result.OcrLine;
OcrResult r = OfficiaOcr.analyze(imageBytes,
OcrOptions.defaults().setLanguage(Language.CHINESE));
r.text(); // 整页文本(行间以 \n 连接)
r.lineCount(); // 行数
r.confidence(); // 平均置信度
r.skewAngleDeg(); // 检测到的倾斜角(绝对值太小时不会真旋转——旋转有损)
for (OcrLine line : r.lines()) {
line.text(); // 该行文本
line.confidence(); // 该行置信度
line.box(); // 该行在原图中的位置(x / y / width / height)
}
// 滤掉低置信度的行(对纯噪声区域有效,见下方警告)
OcrResult clean = r.filterByConfidence(0.5);票据号 / 编号:给字符白名单
已知内容只可能是数字、或只可能是某几个字符时,给白名单是提准最划算的一招:
// 只认数字
String no = OfficiaOcr.recognize(png, OcrOptions.digitsOnly());
// 自定义字符集
String code = OfficiaOcr.recognize(png,
OcrOptions.defaults().setCharWhitelist("0123456789ABCDEF-"));白名单会切换识别路径
给了 charWhitelist(或显式指定字体)时,门面会改走模板匹配而不是神经网络: 候选被塌缩到白名单内,票据/编号场景更准。反过来,通用中文文档不要给白名单, 那会让它退到需要字体同源的模板匹配路径上,反而更差。
扫描件 PDF → 可编辑 Word
整本扫描书稿的入口。注意与 OfficiaPdf.toWord 的区别:后者要求 PDF 有文字层, 扫描件没有文字层,必须走这里。
import plus.ruoyi.officia.ocr.scan.ScannedPdfConverter;
byte[] docx = new ScannedPdfConverter()
.language(Language.CHINESE) // 必填,见下
.toWord(pdfBytes);
// 老书常见版面:扫描时页面横放、两页并排印在一张上
byte[] docx2 = new ScannedPdfConverter()
.language(Language.CHINESE)
.rotate(ScannedPdfConverter.Rotation.CLOCKWISE_90)
.splitFacingPages(true) // 按最大空白带定位中缝,不是对半切
.minConfidence(0.01) // 滤掉插图/印章被误当文本行的噪声
.toWord(pdfBytes);🔴 三条必读的使用约定
OCR 与套件里其它能力有个根本区别:它配错了不会报错。识别器在固定字符集上 永远给出某个类别,没有「认不出」这一档。以下三条都是实测踩出来的。
① 语种必须显式指定
new ScannedPdfConverter().toWord(pdf); // ❌ 直接抛异常OcrOptions 的默认语种是英文。中文扫描件走英文模型不会报错, 只会吐出满篇拉丁噪声,而且平均置信度反而更高——按置信度过滤会被它骗过去。
实测:一本 183 页的中文书就这么白转出 25 万字垃圾,8.6 分钟跑完、零报错。 所以
ScannedPdfConverter宁可在你不表态时响亮地失败。
② 版面参数配错同样不报错
rotate / splitFacingPages 不对时照样返回正常结果,只是内容全是怪字。 同一本对开扫描书、同一份权重,只差这两个参数:
| 参数 | 产物 |
|---|---|
| 不给 | 5.9 KB,全是乱码 |
rotate + splitFacingPages | 285.8 KB,9330 段、17.6 万字可读 |
每本书都要先转两三页看看输出是不是人话,再决定整本怎么转。
③ 置信度不是「对不对」
confidence 高不代表正确。实测一处生僻字把「七曜齐」认成「七隆齐」, 置信度 0.972 —— 它对错字也可以很自信。
minConfidence 能滤掉纯噪声区域(插图、印章被误当文本行,实测那类得分在 0.0001 量级), 滤不掉「认错字」。要验收质量只能对照原文,不能看分数。
授权与评估版限制
未授权的评估态下,识别结果最多输出 1000 行(按实测约 25 行/页折算约 40 页), 超出部分截断,并在末尾追加一行显式提示:
【未授权评估版:识别结果已截断至 1000 行 · 授权联系QQ770492966】为什么是限行数而不是水印
套件里其它能力的评估降级是「限 30 页 + 盖水印」,但那对 OCR 两条都失效: recognize(图片) 没有「页」的概念;而往识别文本里插水印字就是污染数据, 你拿不到干净结果,也就无从评估质量。
改用限行数后,1000 行以内的结果与授权版逐字一致——评估者能真实判断识别质量, 这正是 OCR 最需要被信任的地方。
截断是显式的:提示行会出现在 OcrResult.text() 与产出的 docx 里, 不会让你误以为是识别丢字。
识别质量(实测,非宣称)
| 口径 | 数值 | 题目 |
|---|---|---|
| 留出字体 CER | 0.0107 | 7 款训练时未见过的字体 × 6720 字,只考字形泛化 |
| 中文验证集 CER | 0.0162 | 整行完全正确率 89.4% |
| 英文验证集 CER | 0.0013 | — |
| 2004 年胶印书 | 正文基本逐字准确 | 平均置信度 0.961 |
| 1984 年扫描书 | 召回 85.9% | 30 页实测,与 Tesseract 同口径比对(见下) |
| 1990 年铅印书 | 召回 69.9% | 能读懂大意,不宜直接交付 |
老铅印件差距主要来自字形域差(老铅字的墨色扩散、纸张透印在训练分布外)。 交付前请用你自己的样本实测,别按单一数字预期。
2026-08 的一次取舍:留出字体 CER 从 0.0071 上升到 0.0107
这是有意为之。修正训练语料的字符配比后(拉丁字母此前被数字严重挤占), 真实扫描件上的一类高频误识大幅减少——o→6、l→1、a→8、S↔s 这四类合计从 93 次降到 21 次(-77%),两本真实书的 CER 也都下降。
代价是干净渲染的黑体系汉字字形分辨力下降(退化集中在等线、宋体、雅黑、黑体; 仿宋、楷体反而改善)。因为拉丁字母与黑体系汉字同为横平竖直的简单笔画, 低分辨率下更易混淆。
我们选择了对真实扫描件更有利的一侧——那才是 OCR 的实际输入。
与 Tesseract 的同口径对比
2026-08 拿一本 1984 年的扫描书(526 页)做过一次同口径比对: 同一份扫描图、同样的 30 页、同一套评分脚本。
| 口径 | 数值 |
|---|---|
| 召回 | 85.9% |
| CER | 0.1633 |
| 扣除字表缺口后的可达召回 | 88.4% |
| 速度 | 31 秒/页(纯 CPU) |
这个数字不是「准确率」
参照用的是 Internet Archive 提供的 Tesseract 5.3.0 输出,不是人工校对的真值—— 它自己也会认错(实测「陀阁逝」被它认成 pew)。所以上表只能读作 两个引擎在同一批页上的相对表现,不能理解成「officia 的准确率是 85.9%」。
要评估你自己的场景,唯一可靠的办法是拿你的样本实测。
当前剩余误差主要在标点与拉丁字母(中西文标点互混、半角全角、l/1 与 o/0 形近), 汉字识别本身已较稳定。
能力边界
| 场景 | 状态 |
|---|---|
| 版面还原 | 只做「行 → 段落」的线性还原,不还原表格、多栏、图文混排 |
| 竖排文字 | ❌ 不支持。切分按横排设计(水平投影切行),竖排页面整页只切出 1 行 |
| 两页并排 | ⚠️ 必须显式开 splitFacingPages(true),否则静默产出垃圾 |
| 页面扫描黑边 | ✅ 自动剥离;装订阴影呈渐变且不贯穿时仍可能残留 |
| 手写体 / 艺术字 | ❌ 不支持 |
| 倾角 > 15° | ❌ 属页面朝向错误,请先旋转页面 |
| 速度 | 纯 CPU 推理,一页约数十秒(无 GPU 依赖,也无法用 GPU 加速) |
常见问题
- 要不要在服务器上装 Tesseract? 不需要。推理内核与权重都在 jar 里, 引依赖即可用,不装任何 OCR 软件、不联网。
OfficiaPdf.toWord报"没有文字层":那是扫描件的预期行为,改用ScannedPdfConverter。- 转出来全是怪字:先查语种,再查
rotate/splitFacingPages(见上方约定 ①②)。 - 模型能换吗:可以,
OcrOptions.setModelBytes(byte[])接受自定义.ofm权重; 传了坏模型会抛异常而不是静默退回内置模型。