简介:Java Tess4J Demo 面向需要在 Java 应用中集成 OCR 能力的开发者,尤其适合刚接触 Tesseract 与 Tess4J 的初中级工程师。资源以可运行示例为核心,演示如何通过 Maven 或 Gradle 引入依赖、设置 tessdata 数据路径与识别语言,并调用 doOCR 完成图像文字提取,同时覆盖识别区域设定、自定义词典、psm 与 oem 参数调优等进阶用法。压缩包共 80 个文件,约 21.21MB,包含 14 个 java 源码、44 个 class 编译文件、4 个 jar 依赖、6 个 dll 本地库,以及 traineddata 语言数据、tif/bmp/png/gif 测试图像和 hocr、pdf 输出样例,便于直接运行与对照调试。目前已有 914 人学习下载,可帮助读者快速掌握 Tess4J 配置流程、多格式图像处理与识别效果优化思路。
1. Java Tess4J Demo:从一张模糊截图到可检索文本的完整路径
手里有一批扫描件、截图或者手机拍的文档,想用 Java 把它们批量转成可搜索的文本,这是很多后台系统、档案管理、票据识别场景里绕不开的需求。Tess4J 就是 Java 生态里调用 Tesseract OCR 引擎最直接的封装库,它把本地动态库、语言包、图像预处理入口都串起来了。但真正动手时会发现,Demo 跑通只要十分钟,识别率从 60% 提到 90% 却要折腾好几天。这篇笔记按「环境搭起来 → 图像怎么喂 → 参数怎么调 → 坑在哪」的顺序,把 Java Tess4J Demo 从能跑到能用之间的那段路讲清楚。适合已经会写 Java、手里有真实图片、不想只停留在 Hello World 的开发者。
2. 把 Tess4J 跑起来:依赖、语言包与最小可运行类
2.1 Maven 依赖与本地引擎的三种引入方式
Tess4J 本身是 Java 层封装,底层还是 Tesseract 的本地库。常见做法是用 Maven 拉 jar,再让它在运行时找到 tessdata 和动态库。最省事的写法是引入带分类器的完整包,但体积会大不少。
<dependency> <groupId>net.sourceforge.tess4j</groupId> <artifactId>tess4j</artifactId> <version>5.11.0</version> </dependency>版本号按你仓库里实际能拉到的写,不要照抄。引入之后有三个关键路径要确认:TESSDATA_PREFIX指向语言包目录,jna.library.path指向本地库目录,临时目录要有写权限。我一般会在启动参数里显式指定,而不是依赖系统环境变量,因为部署到容器里环境变量经常丢。
java -Djava.io.tmpdir=/data/tmp \ -Djna.library.path=/opt/tesseract/lib \ -Dfile.encoding=UTF-8 \ -jar ocr-demo.jarjna.library.path是 JNA 找libtesseract和liblept的地方,Linux 下通常是/usr/lib或编译安装后的lib目录。java.io.tmpdir容易被忽略,Tess4J 在处理图像时会往临时目录写中间文件,容器里如果/tmp只读或者空间很小,会报一些看起来和 OCR 无关的 IO 异常。
2.2 最小可运行类:从 BufferedImage 到字符串
先写一个不涉及任何预处理的最小类,目的是确认引擎、语言包、图像读取这条链路是通的。跑通它再谈优化,否则后面调参时你分不清是参数问题还是环境问题。
import net.sourceforge.tess4j.ITesseract; import net.sourceforge.tess4j.Tesseract; import net.sourceforge.tess4j.TesseractException; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; public class OcrMinimal { public static void main(String[] args) throws Exception { // 1. 读取图像,这里用 ImageIO,支持 png/jpg/bmp BufferedImage img = ImageIO.read(new File("/data/in/sample.png")); // 2. 创建引擎实例,注意它不是线程安全的 ITesseract engine = new Tesseract(); // 3. 指定语言包目录,目录下要有 chi_sim.traineddata 等文件 engine.setDatapath("/opt/tessdata"); // 4. 设置语言,中文简体用 chi_sim,中英混排用 chi_sim+eng engine.setLanguage("chi_sim+eng"); // 5. 设置页面分割模式,单行文本和整页文档差别很大 engine.setPageSegMode(3); try { String text = engine.doOCR(img); System.out.println(text); } catch (TesseractException e) { // 这里不要只打印 e.getMessage,堆栈里往往有本地库的线索 e.printStackTrace(); } } }setDatapath指向的目录里必须真的有chi_sim.traineddata,文件名大小写敏感。setLanguage用加号拼接多语言时,识别速度会下降,因为引擎要在多个模型间切换,中英混排场景下这是必要代价。setPageSegMode(3)是自动分页,适合整页文档;如果图片里只有一行字,用 7 单行模式会稳很多。doOCR返回的字符串里换行和空格是引擎按版面猜的,不要指望它和原文完全一致。
2.3 语言包与版本匹配:为什么你的中文识别全是乱码
中文识别出乱码,九成不是代码问题,而是语言包和引擎版本不匹配。Tesseract 4.x 和 5.x 的 traineddata 格式有差异,用旧包配新库,或者反过来,都会出现识别结果全是符号或者直接抛异常。
| 检查项 | 正确状态 | 常见错误 |
|---|---|---|
| traineddata 版本 | 与本地库主版本一致 | 4.x 包配 5.x 库 |
| 文件权限 | 运行用户可读 | 容器内 root 下载、非 root 运行 |
| 目录结构 | datapath 下直接放 .traineddata | 多套了一层 tessdata 目录 |
| 语言代码 | chi_sim / eng / chi_sim+eng | 写成 zh 或 chinese |
我一般会先用命令行tesseract --version和tesseract --list-langs确认本地引擎能识别到哪些语言,再让 Java 用同一个 datapath。如果命令行能识别、Java 不能,问题就在setDatapath的路径上,而不是语言包本身。
3. 图像预处理:让 Tess4J 识别率翻倍的几个动作
3.1 灰度、二值化与去噪的 Java 实现
Tesseract 对输入图像的质量比想象中敏感。彩色扫描件直接丢进去,识别率往往不如先转灰度再二值化。下面这段用 Java 原生 BufferedImage 做灰度化和固定阈值二值化,不引入 OpenCV,适合不想加额外依赖的场景。
import java.awt.image.BufferedImage; public class ImagePreprocess { // 灰度化:按人眼感知加权,比简单平均更接近真实亮度 public static BufferedImage toGray(BufferedImage src) { int w = src.getWidth(), h = src.getHeight(); BufferedImage gray = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { int rgb = src.getRGB(x, y); int r = (rgb >> 16) & 0xFF; int g = (rgb >> 8) & 0xFF; int b = rgb & 0xFF; int v = (int) (0.299 * r + 0.587 * g + 0.114 * b); gray.setRGB(x, y, (v << 16) | (v << 8) | v); } } return gray; } // 固定阈值二值化,threshold 一般取 150~180 public static BufferedImage binarize(BufferedImage gray, int threshold) { int w = gray.getWidth(), h = gray.getHeight(); BufferedImage bin = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_BINARY); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { int v = gray.getRGB(x, y) & 0xFF; int out = v < threshold ? 0x000000 : 0xFFFFFF; bin.setRGB(x, y, out); } } return bin; } }灰度化用加权公式而不是(r+g+b)/3,是因为人眼对绿色更敏感,加权后文字和背景的对比更接近真实观感。二值化阈值没有万能值,白底黑字的文档 160 左右通常不错,泛黄的老扫描件可能要降到 120。固定阈值最大的问题是遇到光照不均就翻车,这时候要么换自适应阈值,要么先做背景估计再相减。
3.2 分辨率、DPI 与缩放:Tesseract 的隐形门槛
Tesseract 内部对字符高度有预期,官方建议输入图像的字符高度在 30 到 40 像素之间。手机拍的文档如果整页只有 800 像素宽,单字可能只有 10 像素高,识别率会断崖式下跌。
import java.awt.image.BufferedImage; import java.awt.Graphics2D; import java.awt.RenderingHints; public class ResizeUtil { // 按倍数放大,scale=2 表示宽高各乘 2 public static BufferedImage scale(BufferedImage src, double scale) { int w = (int) (src.getWidth() * scale); int h = (int) (src.getHeight() * scale); BufferedImage dst = new BufferedImage(w, h, src.getType()); Graphics2D g = dst.createGraphics(); // 双三次插值在放大文字时比默认的最近邻平滑得多 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); g.drawImage(src, 0, 0, w, h, null); g.dispose(); return dst; } }放大不是越大越好。放到 3 倍以上,边缘锯齿和噪点也被放大,识别率反而可能下降。我一般先估算原图里字符的大致高度,再决定放大倍数,目标是让字符落到 30 到 40 像素这个区间。如果原图本身分辨率够,就不要缩放,直接送进去。
3.3 版面分析:什么时候该切图,什么时候该整页送
整页文档里如果有多栏、表格、页眉页脚,Tesseract 的自动分页经常把阅读顺序搞乱。常见做法是先用版面分析把正文区域切出来,再逐块识别。不引入额外库的话,可以用投影法做粗切。
public class ProjectionCut { // 水平投影:统计每一行黑色像素数量,用于找文本行边界 public static int[] horizontalProjection(BufferedImage bin) { int w = bin.getWidth(), h = bin.getHeight(); int[] proj = new int[h]; for (int y = 0; y < h; y++) { int count = 0; for (int x = 0; x < w; x++) { if ((bin.getRGB(x, y) & 0xFF) == 0) count++; } proj[y] = count; } return proj; } }投影值连续为 0 的区间通常是行间距,可以用来切分文本行。这个方法对规整的印刷文档有效,对倾斜、弯曲的文本就力不从心。如果业务里倾斜扫描件多,老老实实上 OpenCV 做纠偏,别在投影法上耗时间。
4. 参数调优:Tess4J 里真正影响结果的几个开关
4.1 PageSegMode 的取值与适用场景
setPageSegMode是影响识别结果最大的单个参数。它告诉引擎怎么理解图像里的版面结构,选错了,后面的预处理全白做。
| 模式值 | 含义 | 适用场景 |
|---|---|---|
| 3 | 全自动分页 | 整页文档,默认值 |
| 4 | 单列可变大小文本 | 单栏文章、书籍 |
| 6 | 统一文本块 | 规则排版的表单 |
| 7 | 单行文本 | 裁剪后的单行、验证码 |
| 8 | 单词 | 单个词条 |
| 13 | 原始行,绕过引擎内部处理 | 已切好的干净文本行 |
我处理票据时常用 6,因为票据字段位置相对固定;处理书籍扫描用 4;处理截图里的单行报错信息用 7。模式 13 比较特殊,它跳过引擎的很多内部版面处理,适合你已经把图像切得很干净、只想要纯识别的情况。
4.2 字符白名单与黑名单:把识别范围收窄
如果业务里只可能出现数字和少数几个符号,设置字符白名单能显著减少误识别。比如识别金额时,把字母排除掉,引擎就不会把0认成O。
ITesseract engine = new Tesseract(); engine.setDatapath("/opt/tessdata"); engine.setLanguage("eng"); // 只允许数字、小数点、逗号和减号 engine.setVariable("tessedit_char_whitelist", "0123456789.,-"); // 或者排除特定字符 engine.setVariable("tessedit_char_blacklist", "IlO");setVariable是透传 Tesseract 内部变量的入口,变量名和取值要查对应版本文档。白名单设得太窄也有风险,比如金额里偶尔出现¥符号,没加进去就会被丢掉。我的习惯是先不加白名单跑一遍,看误识别集中在哪些字符,再针对性收窄。
4.3 超时、并发与内存:批量处理时的稳定性参数
Tess4J 的ITesseract实例不是线程安全的,多线程共用一个实例会出现各种诡异结果。批量处理时的常见做法是每个线程一个实例,或者用线程池加 ThreadLocal。
import java.util.concurrent.*; public class OcrPool { // 每个线程持有独立引擎,避免共享状态 private static final ThreadLocal<ITesseract> ENGINE = ThreadLocal.withInitial(() -> { ITesseract t = new Tesseract(); t.setDatapath("/opt/tessdata"); t.setLanguage("chi_sim+eng"); t.setPageSegMode(6); return t; }); public static String ocr(BufferedImage img) throws TesseractException { return ENGINE.get().doOCR(img); } public static void main(String[] args) throws Exception { // 并发度不要超过 CPU 核数,OCR 是计算密集型 ExecutorService pool = Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors()); // 提交任务时给每个任务设超时,防止个别图片卡死整个批次 Future<String> f = pool.submit(() -> ocr(ImageIO.read(new File("/data/in/a.png")))); try { System.out.println(f.get(30, TimeUnit.SECONDS)); } catch (TimeoutException e) { f.cancel(true); System.err.println("单张识别超时,跳过"); } pool.shutdown(); } }并发度设成 CPU 核数是比较稳的起点,OCR 吃 CPU 也吃内存,开太多线程反而因为内存交换变慢。超时是必须的,我遇到过个别损坏的图片让引擎卡住几分钟,没有超时保护的话整个批次就挂在那里。内存方面,大图放大后占用会翻几倍,JVM 堆要留够,必要时用-Xmx显式限制。
5. 避坑与排查:那些让 Demo 翻车的细节
5.1 现象:本地跑得好,部署到服务器就报 UnsatisfiedLinkError
原因通常是本地库路径没配对。开发机上 Tesseract 装在系统默认路径,JNA 能自动找到;服务器上装在自定义目录,jna.library.path没指过去。解决方式是在启动脚本里显式加-Djna.library.path=/your/lib/path,并用ldd确认libtesseract.so的依赖都齐全。容器场景下还要注意基础镜像里有没有libgomp、libstdc++这些运行时库。
5.2 现象:中文识别结果里夹杂大量空格和换行
原因是页面分割模式选得不对,引擎把字间距当成了词边界。中文没有天然空格,Tesseract 按英文习惯分词就会插入多余空格。解决方式是中文场景优先用模式 6 或 7,识别完再用正则把中文字符之间的空格去掉。不要指望引擎输出和原文逐字一致,后处理是必要环节。
5.3 现象:同一张图两次识别结果不一样
Tesseract 在某些模式下有随机性,尤其是自适应阈值和版面分析阶段。如果业务要求结果可复现,要固定所有参数,避免使用依赖运行时状态的模式。另外,多线程共享实例也会导致结果不稳定,每个线程独立实例是底线。
5.4 现象:识别速度慢到无法接受
先确认是不是每次都在重新加载语言包。ITesseract实例创建时会加载模型,频繁 new 实例开销很大。正确做法是复用实例,配合 ThreadLocal 解决线程安全问题。其次检查图像尺寸,一张 4000x6000 的图直接送进去,引擎内部处理会非常慢,先缩放到合理尺寸再识别。最后看语言数量,chi_sim+eng比单语言慢不少,如果业务里英文只是偶尔出现,可以考虑先判断再选语言。
5.5 现象:数字 0 和字母 O、1 和 l 混淆
这是 OCR 的经典问题,纯靠引擎很难根治。实用做法是结合业务规则做后处理,比如金额字段只允许数字,识别完把 O 替换成 0、l 替换成 1。更稳的方式是训练针对特定字体的模型,但成本高,一般业务用白名单加规则后处理就够了。
6. 进阶:用置信度过滤与后处理把结果变成可用数据
跑通识别只是第一步,真正落地时你需要知道哪些结果可信、哪些要人工复核。Tess4J 提供了获取识别结果细节的入口,可以拿到每个词的置信度。
import net.sourceforge.tess4j.ITesseract; import net.sourceforge.tess4j.Tesseract; import net.sourceforge.tess4j.Word; import java.util.List; public class ConfidenceFilter { public static void main(String[] args) throws Exception { ITesseract engine = new Tesseract(); engine.setDatapath("/opt/tessdata"); engine.setLanguage("chi_sim+eng"); // getWords 返回每个词的文本、置信度和边界框 List<Word> words = engine.getWords( ImageIO.read(new File("/data/in/sample.png")), 0); StringBuilder sb = new StringBuilder(); for (Word w : words) { // 置信度低于 70 的词标记出来,交给人工复核 if (w.getConfidence() < 70) { sb.append("[?]").append(w.getText()).append(" "); } else { sb.append(w.getText()).append(" "); } } System.out.println(sb); } }getWords的第二个参数是页面迭代器,传 0 表示处理所有页面。置信度阈值没有统一标准,印刷体清晰文档 80 以上比较稳,手写或低质量扫描件可能要降到 60。把低置信度词标记出来,比直接丢弃更有价值,因为人工复核时能快速定位问题区域。
后处理方面,我一般会做三件事:按业务字段做正则校验,比如日期、金额、编号;把常见误识别字符做映射替换;对连续低置信度的区域触发重新识别,换一种预处理参数再跑一遍。这套组合下来,实际业务里的可用率能从裸识别的六七成提到九成以上。
最后说个我自己的习惯:每次调整参数或预处理流程,都留一组固定的测试图片和对应的期望结果,改完跑一遍对比。OCR 调参很容易陷入「感觉变好了」的错觉,没有固定测试集,你根本不知道这次改动是进步还是退步。这套测试集不用大,二三十张覆盖主要场景就够,但一定要有。希望帮到你。
本文还有配套的精品资源,点击获取