简介:这份资源面向需要在 Java 应用中集成 OCR 能力的开发者,提供了一套可直接运行的 Tess4J 示例工程,用于解决图像文字自动识别与提取的问题。压缩包共 80 个文件,约 21.21MB,包含 14 个 java 源码与 44 个 class 编译文件、4 个 jar 依赖库、6 个 dll 本地库,以及 eng.traineddata 语言数据、tif/bmp/png/gif 等多格式测试图像和 pdf、hocr 输出样例,覆盖从源码、依赖到测试素材的完整链路。目前已有 922 人学习下载。工程演示了 Tesseract 实例创建、数据路径与语言设置、doOCR 调用等基础流程,并涉及识别区域设定、自定义词汇文件、psm 与 oem 参数调优等进阶用法,读者可据此快速搭建环境、理解配置要点并迁移到自己的图像识别任务中。
1. 从一张发票说起:Java Tess4J Demo 能解决什么
上周同事抱着一摞报销单来找我,说财务要求把纸质发票上的号码和金额录进系统,人工敲了两百多张,眼睛都花了。这种场景其实特别适合交给 OCR 去做——拍张照或者扫一下,程序自动把文字抠出来。这份 Java Tess4J Demo 就是干这个的:它把 Tesseract 这个老牌 OCR 引擎用 JNA 封装成 Java 能直接调的接口,让你在纯 Java 项目里几行代码就能完成图片文字识别,不用装 Python、不用起独立服务。适合谁?做后台管理系统的、需要批量处理票据/证件/截图文字的 Java 开发者,尤其是那些项目里已经有一堆 Spring Boot 服务、不想再引入异构技术栈的团队。它不追求识别率天花板,胜在集成成本低、离线可跑、可控性强。
2. 技术选型与工程结构:Tess4J 到底封装了什么
2.1 为什么是 Tess4J 而不是调云 API
先说清楚这个 Demo 的技术底座。Tesseract 本身是 C++ 写的 OCR 引擎,Java 没法直接调,Tess4J 的做法是通过 JNA(Java Native Access)去加载本地动态库,把libtesseract和liblept的能力桥接到 JVM 里。所以你拿到的不是一个纯 jar 包,而是「jar + 本地库 + 语言训练数据」三件套。
选它而不是云 OCR API,核心考量有三点。第一是离线,票据、证件这类东西往往不方便往外传,本地跑最省心。第二是成本,云 API 按次计费,批量跑起来账单不好看,本地引擎跑一万张也就费点 CPU。第三是可控,识别参数、预处理流程、语言包都能自己调,出问题能定位到具体环节,不像云服务那样是个黑匣子。
代价也要讲明白:Tesseract 对图片质量敏感,倾斜、模糊、低对比度的图识别率会明显掉;中文识别需要单独下chi_sim.traineddata,而且默认的 LSTM 模型对复杂排版不算友好。这些在后面避坑章节会细说。
2.2 目录结构与依赖清单
一个能跑起来的 Tess4J 工程,目录大致长这样(Maven 结构):
tess4j-demo/ ├── pom.xml ├── tessdata/ # 语言训练数据目录 │ ├── eng.traineddata │ └── chi_sim.traineddata ├── src/main/java/com/example/ocr/ │ ├── OcrService.java # 识别服务封装 │ ├── ImagePreprocessor.java # 图片预处理 │ └── OcrApplication.java # 启动入口 └── src/main/resources/ └── samples/ # 测试图片pom.xml里最关键的依赖就一个,但要注意 Tess4J 的版本和它捆绑的 Tesseract 版本是绑定的:
<dependency> <groupId>net.sourceforge.tess4j</groupId> <artifactId>tess4j</artifactId> <version>5.8.0</version> </dependency>这个版本会连带拉进jna、jai-imageio、leptonica等传递依赖。常见做法是显式锁定 JNA 版本,避免和项目里其他用到 JNA 的库(比如某些硬件 SDK)打架。我一般会在dependencyManagement里把net.java.dev.jna:jna钉死到5.13.0左右,省得后面出现UnsatisfiedLinkError这种玄学问题。
tessdata目录的位置很讲究。Tess4J 默认会去几个固定路径找训练数据,但最稳的做法是在代码里显式setDatapath()指过去,别指望它自己猜。语言包从 Tesseract 官方仓库下,eng和chi_sim各几十兆,放项目里或者放服务器固定目录都行,后者更适合多服务共用。
2.3 一次识别的完整调用链路
把链路拆开看,一次doOCR调用内部走了这么几步:JNA 加载本地库 → 初始化TessBaseAPI→ 设置 datapath 和语言 → 把BufferedImage转成 Leptonica 的PIX结构 → 调TessBaseAPI::Recognize→ 取回文本。理解这条链路的价值在于:任何一步出问题,报错信息都能对得上号。比如UnsatisfiedLinkError是第一步挂了,Error opening data file是 datapath 或语言包问题,识别出来是乱码则多半是图片预处理或语言设置的问题。
3. 跑起来:从零到识别出第一行字
3.1 环境准备与本地库加载
先确认 JDK 版本,Tess4J 5.x 要求 JDK 8 以上,我实测 JDK 11 和 17 都正常。操作系统层面,Windows 和 Linux 都能跑,但本地库的加载方式略有差别。
Windows 下 Tess4J 的 jar 里自带了win32-x86-64的 dll,一般不用额外装东西。Linux 下就得多留个心眼,系统里需要有libtesseract和liblept的 so 文件,缺了会直接抛链接错误。Debian 系可以这样补:
# 安装 Tesseract 运行时库和语言数据 sudo apt-get install -y libtesseract-dev libleptonica-dev tesseract-ocr # 单独装中文语言包 sudo apt-get install -y tesseract-ocr-chi-sim装完可以用tesseract --version验证一下,能打印版本号说明运行时没问题。注意这里装的是系统级 Tesseract,Tess4J 在找不到自带库时会回退到系统库,两条路都通更保险。
3.2 最小可运行示例
下面这段是能直接抄的识别入口,我把关键参数都标了注释:
import net.sourceforge.tess4j.Tesseract; import net.sourceforge.tess4j.TesseractException; import java.io.File; public class OcrApplication { public static void main(String[] args) { Tesseract tesseract = new Tesseract(); // 指向训练数据目录,结尾不要带斜杠 tesseract.setDatapath("tessdata"); // 中文简体 + 英文,多语言用加号连接 tesseract.setLanguage("chi_sim+eng"); // 识别引擎模式:3 表示全自动分页分析,适合大多数场景 tesseract.setPageSegMode(3); // 保留换行和空格,票据类文本需要 tesseract.setVariable("preserve_interword_spaces", "1"); try { String result = tesseract.doOCR(new File("samples/invoice.png")); System.out.println(result); } catch (TesseractException e) { // 识别失败通常是图片格式或本地库问题 System.err.println("OCR failed: " + e.getMessage()); } } }逐项说下参数。setDatapath是训练数据根目录,里面必须能直接找到chi_sim.traineddata这类文件,多套一层子目录就会报Error opening data file。setLanguage用+拼多个语言,顺序会影响优先级,中文票据建议chi_sim+eng,因为金额里的数字和字母用英文模型识别更准。setPageSegMode(3)是 PSM_AUTO,让引擎自己判断版面;如果是纯单行文本(比如验证码),改成 7(单行)识别率会更高。preserve_interword_spaces这个变量容易被忽略,但票据里字段之间靠空格对齐,不设的话输出会挤成一坨。
3.3 用预处理把识别率拉上来
直接拿原图喂进去,十有八九效果一般。我一般会在doOCR之前加一道预处理,核心就三件事:灰度化、二值化、去噪。下面这段用BufferedImage手写,不额外引 OpenCV:
import java.awt.image.BufferedImage; import java.awt.Color; public class ImagePreprocessor { public static BufferedImage toBinary(BufferedImage src, int threshold) { int w = src.getWidth(), h = src.getHeight(); BufferedImage out = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_BINARY); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { Color c = new Color(src.getRGB(x, y)); // 加权灰度:人眼对绿色更敏感,权重按亮度公式取 int gray = (int) (c.getRed() * 0.299 + c.getGreen() * 0.587 + c.getBlue() * 0.114); // 高于阈值置白,否则置黑 out.setRGB(x, y, gray > threshold ? 0xFFFFFF : 0x000000); } } return out; } }threshold这个参数没有万能值,浅色背景的票据我一般从 160 起调,深色背景的截图可能要到 200。判断标准很简单:二值化后的图,文字边缘清晰、没有大片粘连就算合格。如果原图本身分辨率太低(小于 300 DPI),先放大再二值化,Tesseract 对小于 20 像素高的字识别很差。这一步做完再调doOCR,中文票据的识别率通常能从「勉强能看」提到「基本可用」。
4. 核心模块拆解:服务封装与结果处理
4.1 把 Tesseract 包成线程安全的服务
Tesseract实例本身不是线程安全的,多个请求共用一个实例会出各种诡异结果。生产环境里我一般用ThreadLocal或者对象池来管,下面这个封装是ThreadLocal版本:
public class OcrService { // 每个线程独立实例,避免并发串数据 private static final ThreadLocal<Tesseract> HOLDER = ThreadLocal.withInitial(() -> { Tesseract t = new Tesseract(); t.setDatapath("tessdata"); t.setLanguage("chi_sim+eng"); t.setPageSegMode(3); return t; }); public String recognize(BufferedImage image) throws TesseractException { // 预处理后再识别,阈值可按业务配置化 BufferedImage binary = ImagePreprocessor.toBinary(image, 170); return HOLDER.get().doOCR(binary); } public void cleanup() { // 线程池场景下记得清理,防止内存泄漏 HOLDER.remove(); } }这里有两个设计点值得说。一是ThreadLocal.withInitial保证每个线程第一次用时才初始化,避免启动时集中加载本地库拖慢启动。二是cleanup()方法,如果你用的是线程池,线程复用会导致ThreadLocal里的实例一直挂着,虽然单个Tesseract占内存不算大,但配合大图缓存还是会累积,定时清理是血泪经验。
4.2 识别结果的清洗与结构化
doOCR返回的是纯文本,票据场景下还得做字段抽取。常见做法是用正则按关键字定位,比如金额、发票号:
import java.util.regex.Matcher; import java.util.regex.Pattern; public class ResultParser { // 匹配「金额」「合计」后面跟的数字,容忍中间的空格和冒号 private static final Pattern AMOUNT = Pattern.compile("(?:金额|合计)[::\\s]*([0-9]+(?:\\.[0-9]{1,2})?)"); private static final Pattern INVOICE_NO = Pattern.compile("(?:发票号码|No)[::\\s]*([0-9]{8,20})"); public static String extractAmount(String text) { Matcher m = AMOUNT.matcher(text); return m.find() ? m.group(1) : null; } public static String extractInvoiceNo(String text) { Matcher m = INVOICE_NO.matcher(text); return m.find() ? m.group(1) : null; } }正则里的[::\\s]*是关键,OCR 输出里冒号可能是全角也可能是半角,中间还常夹空格,不兼容这些变体就会漏匹配。金额部分限制[0-9]{1,2}位小数,避免把后面的无关数字吞进来。这套解析逻辑的定位是「辅助人工复核」,不是全自动入账——OCR 一定有错,字段抽取的价值是把人工从「逐字看」降到「核对关键字段」,这个预期要提前跟业务方对齐。
4.3 批量处理的并发与限流
批量跑几百上千张图时,直接parallelStream一把梭容易把 CPU 打满、内存飙高。我一般用固定大小线程池加信号量控制并发:
import java.util.concurrent.*; public class BatchOcr { // 并发数按 CPU 核数的一半起步,OCR 是计算密集型 private static final ExecutorService POOL = Executors.newFixedThreadPool(Math.max(2, Runtime.getRuntime().availableProcessors() / 2)); public static void process(java.util.List<java.io.File> files) throws Exception { OcrService service = new OcrService(); CountDownLatch latch = new CountDownLatch(files.size()); for (java.io.File f : files) { POOL.submit(() -> { try { // 读图 -> 识别 -> 落库,异常单独捕获不影响其他任务 System.out.println(f.getName() + " => " + service.recognize(javax.imageio.ImageIO.read(f))); } catch (Exception e) { System.err.println("skip " + f.getName() + ": " + e.getMessage()); } finally { latch.countDown(); } }); } latch.await(); POOL.shutdown(); } }并发数别贪多,Tesseract 单次识别会吃满一个核,开太多线程反而因为上下文切换变慢。实测 8 核机器开 4 个线程吞吐最好,再往上收益递减。另外每张图识别完记得让BufferedImage及时失去引用,大图(比如 4000×3000 的扫描件)单张就占几十兆,批量跑不控制内存很容易 OOM。
5. 避坑与排查:那些让我加班到深夜的问题
5.1 报错UnsatisfiedLinkError找不到本地库
现象是启动就崩,堆栈里一堆java.lang.UnsatisfiedLinkError: Unable to load library 'tesseract'。原因通常是运行环境和打包方式不匹配:Windows 开发、Linux 部署,jar 里带的 dll 在服务器上没用;或者用了shade插件把 jar 打成一个 fat jar,本地库文件被覆盖或丢失。解决办法是部署环境单独装系统级 Tesseract(前面 3.1 的命令),让 Tess4J 回退到系统库;打包时确认tess4j的 native 资源没被过滤掉,maven-shade-plugin里要显式 include**/*.dll和**/*.so。
5.2 中文识别全是乱码或方块
现象是英文数字正常,中文出来一堆问号或乱码。原因基本是语言包没加载对:要么tessdata目录里根本没有chi_sim.traineddata,要么setLanguage写成了chi而不是chi_sim。还有个隐蔽情况是训练数据版本和 Tesseract 引擎版本不匹配,老版traineddata喂给新版引擎会静默失败。解决方式是确认文件名精确为chi_sim.traineddata、setLanguage("chi_sim"),并且从与引擎版本对应的官方仓库下载语言包。
5.3 识别结果顺序错乱、字段串行
现象是明明图上金额在右边,识别出来却跑到发票号后面去了。原因是PageSegMode选得不对,默认的自动分页对复杂表格会按它自己的理解重排。解决办法是针对票据类固定版式,改用 PSM 6(假设是一整块统一文本)或者先用图像裁剪把字段区域切出来分别识别。我一般会先跑一遍看输出顺序,再决定是调 PSM 还是加区域裁剪,后者虽然麻烦但最稳。
5.4 大批量处理时内存持续上涨直至 OOM
现象是跑了几百张后堆内存曲线一路向上,GC 也压不住。原因有两个:一是ThreadLocal里的Tesseract实例没清理,二是BufferedImage和 Leptonica 的PIX结构在 JNA 层有 native 内存,Java 堆看不出来但进程 RSS 一直涨。解决办法是线程池任务结束后调cleanup(),并且限制单张图的最大尺寸(超过 3000 像素先缩放),native 内存这块只能靠控制并发和及时释放来压。
5.5 同一张图两次识别结果不一样
现象是同一张图跑两次,个别字符识别结果不同。原因是 Tesseract 内部有多线程和自适应分类器,某些边界情况下结果会有微小抖动,尤其在低质量图片上。这不是 bug,是引擎特性。解决办法是对关键字段做「多次识别取众数」或者加置信度过滤——Tess4J 可以通过getWords()拿到每个词的置信度,低于阈值的字段标记为「需人工复核」,比盲目相信单次结果靠谱得多。
6. 二次开发:把 Demo 改成能上生产的 OCR 服务
拿到这个 Demo 之后,真正要落地还得做几件事,我按优先级排一下。
第一件是配置外置。datapath、language、threshold、并发数这些别写死在代码里,抽到application.yml或者配置中心,不同业务线用不同语言包和阈值。第二件是结果缓存。同一张图重复识别的场景很常见(用户刷新、重试),用图片 MD5 做 key 缓存识别结果,能省掉大量重复计算。第三件是置信度驱动的复核流程,这是把 OCR 从「玩具」变成「工具」的关键:
// 取词级置信度,低于阈值的标记待复核 var words = tesseract.getWords(image, ITessAPI.TessPageIteratorLevel.RIL_WORD); for (var w : words) { if (w.getConfidence() < 60) { // 置信度低于 60 的词进入人工复核队列 reviewQueue.add(w.getText()); } }置信度阈值 60 是个经验值,票据类可以放宽到 70,手写体场景可能得降到 40 并接受更高的人工介入率。这个机制的价值在于把「全自动」变成「自动 + 抽检」,业务方接受度会高很多。
验证方法上,我习惯准备一个 50 张左右的小测试集,覆盖清晰扫描件、手机拍照、倾斜、低对比度几类,每次改预处理参数或升级版本都跑一遍,记录字段级准确率而不是整体字符准确率——后者好看但没意义,业务只关心金额和号码对不对。
最后说个我踩过的坑:早期我图省事,把Tesseract实例做成 Spring 单例 Bean,压测时并发一上来结果就开始串,排查了大半天才定位到线程安全问题。从那以后我每次封装这类带 native 状态的组件,都强制先过一遍并发测试再上线。希望这篇拆解能帮你少走点弯路,把这份 Demo 顺顺当当跑起来、改下去。
本文还有配套的精品资源,点击获取