简介:这份资源面向需要在 Java 应用中集成 OCR 能力的开发者,尤其是刚接触 Tesseract 与 Tess4J 的初中级程序员。它通过一个可运行的示例工程,演示如何调用 Tess4J 访问 Tesseract 引擎,把图片中的文字转换为可编辑、可搜索的文本,覆盖依赖引入、语言数据路径设置、识别语言切换等基础环节,并为进一步处理识别区域、自定义词典与调优参数留出扩展空间。压缩包共 80 个文件,约 21.21MB,以 class 与 java 源码为主体,辅以 dll 本地库、jar 依赖包、traineddata 语言数据及 tif、bmp、png 等测试图像,构成一套可直接导入运行的完整工程。目前已有 922 人学习下载。读者可从中获得可复用的工程骨架、依赖配置参考与多格式图像测试样例,便于快速跑通 OCR 流程并对照调整识别效果。
1. 从一张发票说起:为什么我盯上了 Tess4J 这个 OCR 方案
上个月接了个内部小工具的需求,业务方丢过来一摞扫描件,要求把上面的编号和金额自动抠出来录进系统。第一反应是调云服务,但对方明确说数据不能出内网,只能本地跑。于是我把目光转回了 Tesseract 这条老路子,而 Java 侧最省事的入口就是 Tess4J——它本质上是 Tesseract 的 JNA 封装,让你在 JVM 里直接调 C++ 的 OCR 引擎,不用自己写 JNI。这个 Demo 项目干的事很朴素:把「图片进、文字出」这条链路用最小可运行的代码跑通,适合两类人——一类是像我这样需要在离线环境里做 OCR 的后端开发,另一类是刚接触 Tess4J、想先看个能跑的例子再动手改的工程师。它不解决所有识别精度问题,但能让你在半小时内看到第一行识别结果。
2. 环境与依赖:把 Tesseract 原生库和语言包先摆平
Tess4J 最容易翻车的地方从来不是 Java 代码,而是原生库和语言数据。很多人mvn一跑就报UnsatisfiedLinkError,本质是 JVM 找不到libtesseract或liblept。这一章先把运行环境铺平,再谈代码。
2.1 依赖坐标与版本选择
Maven 项目里核心就一个依赖,但要注意 Tess4J 的版本和它内置的 Tesseract 版本是绑定的。常见做法是选一个较新的稳定版,比如 5.x 系列,它对应 Tesseract 5 的 API。pom.xml里加这么一段:
<dependency> <groupId>net.sourceforge.tess4j</groupId> <artifactId>tess4j</artifactId> <version>5.11.0</version> </dependency>这个坐标会连带拉进 JNA、Leptonica 的 Java 绑定等传递依赖。参数说明:version不要随手写LATEST,因为不同小版本对语言包路径的默认查找逻辑有差异,锁死一个能跑的版本,后面排查问题才有基准。如果你用的是 Gradle,换成implementation 'net.sourceforge.tess4j:tess4j:5.11.0'即可,逻辑一样。
2.2 原生库与 tessdata 的落位
Tesseract 运行时需要两样东西:动态库(Windows 下是libtesseract.dll、liblept.dll,Linux 下是.so)和语言数据目录tessdata。Tess4J 的 jar 里其实带了部分平台的库,但语言包通常要你自己放。我一般会在项目根目录建一个tessdata文件夹,把eng.traineddata、chi_sim.traineddata丢进去,然后在代码里显式指定路径,而不是依赖默认查找。
# 目录结构示意 project-root/ ├── tessdata/ │ ├── eng.traineddata │ └── chi_sim.traineddata ├── src/main/java/... └── pom.xml注意:traineddata文件要和 Tesseract 主版本匹配,5.x 的引擎读 4.x 的语言包有时能跑但精度会掉,这是血泪经验。Linux 服务器上如果系统已经装了tesseract-ocr,也可以直接指向/usr/share/tesseract-ocr/5/tessdata,省得自己维护一份。
2.3 一个最小可运行的识别入口
环境就绪后,核心代码其实短得可怜。下面这段就是 Demo 的主干,读图、设语言、调doOCR:
import net.sourceforge.tess4j.Tesseract; import net.sourceforge.tess4j.TesseractException; import java.io.File; public class OcrDemo { public static void main(String[] args) { Tesseract tesseract = new Tesseract(); // 显式指定语言包目录,避免默认路径找不到 tesseract.setDatapath("tessdata"); // 中英混排时用加号连接,顺序影响不大但建议英文在前 tesseract.setLanguage("eng+chi_sim"); try { File image = new File("sample.png"); String result = tesseract.doOCR(image); System.out.println(result); } catch (TesseractException e) { // 识别失败通常是图片格式或语言包问题,打印堆栈便于定位 e.printStackTrace(); } } }逻辑说明:setDatapath指向的是包含traineddata的父目录,不是文件本身;setLanguage传eng+chi_sim表示同时加载两套语言模型,引擎会做多语言融合,代价是内存和耗时上升。doOCR(File)是最简单的重载,内部会把文件转成BufferedImage再走doOCR(BufferedImage)。参数上,如果你只有英文场景,就别加载中文包,启动速度和准确率都会好一些。
3. 核心链路拆解:从 BufferedImage 到字符串的每一步
跑通之后就得看它内部到底干了什么,不然调参全靠玄学。Tess4J 的调用链不复杂,但几个关键节点决定了你后面能不能做预处理和结果清洗。
3.1 图像输入与预处理接口
doOCR接受File、BufferedImage、URL等多种输入,最终都会归一成BufferedImage。这意味着你可以在送进去之前自己做灰度、二值化、缩放。Demo 里如果只传File,等于把预处理权交给了 Tesseract 内部那套通用逻辑,对扫描件往往不够。
import net.sourceforge.tess4j.Tesseract; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; public class PreprocessDemo { public static void main(String[] args) throws Exception { BufferedImage raw = ImageIO.read(new File("sample.png")); // 转灰度:OCR 对色彩不敏感,灰度能减少干扰 BufferedImage gray = new BufferedImage( raw.getWidth(), raw.getHeight(), BufferedImage.TYPE_BYTE_GRAY); gray.getGraphics().drawImage(raw, 0, 0, null); Tesseract tesseract = new Tesseract(); tesseract.setDatapath("tessdata"); tesseract.setLanguage("eng"); // 直接送 BufferedImage,预处理由自己掌控 String text = tesseract.doOCR(gray); System.out.println(text); } }这里的关键参数是BufferedImage.TYPE_BYTE_GRAY,它把三通道压成单通道,Tesseract 内部做二值化时噪声更少。实际项目里我还会加一步按 DPI 缩放,因为 Tesseract 对 300 DPI 左右的图最友好,太小会糊、太大反而慢。
3.2 识别参数:PageSegMode 与变量白名单
Tesseract实例上可以设setPageSegMode,控制它怎么理解版面。默认是AUTO,对整页文档还行,但对单行编号、表格单元格就容易切错。常见做法是单行场景用SINGLE_LINE,单块用SINGLE_BLOCK。
import net.sourceforge.tess4j.Tesseract; import net.sourceforge.tess4j.ITessAPI; Tesseract tesseract = new Tesseract(); tesseract.setDatapath("tessdata"); tesseract.setLanguage("eng"); // 只识别单行,适合发票号、身份证号这类场景 tesseract.setPageSegMode(ITessAPI.TessPageSegMode.PSM_SINGLE_LINE); // 限定字符集,减少把 0 认成 O 的概率 tesseract.setVariable("tessedit_char_whitelist", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");PSM_SINGLE_LINE告诉引擎「这一整张图就是一行」,省掉版面分析的开销,也避免它把数字拆成多段。tessedit_char_whitelist是变量注入,只允许这些字符出现,对纯数字编号场景效果立竿见影。注意白名单别设太窄,否则遇到分隔符会被直接吞掉。
3.3 结果后处理与置信度
doOCR返回的是纯字符串,但 Tess4J 还提供了getWords()之类的方法拿词级结果和置信度。Demo 里如果只打印字符串,等于丢掉了质量信号。我一般会在关键字段上做一次置信度过滤,低于阈值的走人工复核。
import net.sourceforge.tess4j.Tesseract; import net.sourceforge.tess4j.Word; import java.io.File; import java.util.List; Tesseract tesseract = new Tesseract(); tesseract.setDatapath("tessdata"); tesseract.setLanguage("eng"); List<Word> words = tesseract.getWords(new File("sample.png"), ITessAPI.TessPageIteratorLevel.RIL_WORD); for (Word w : words) { // 置信度低于 60 的词标记出来,后续人工看 if (w.getConfidence() < 60) { System.out.println("[低置信] " + w.getText() + " -> " + w.getConfidence()); } }RIL_WORD表示按词粒度返回,getConfidence()是 0 到 100 的浮点。这个接口在批量处理时很有用,能让你把「机器没把握」的部分单独拎出来,而不是整批结果一起信或一起不信。
4. 避坑与排查:那些让 Demo 跑不起来的常见问题
这一章是我踩过的坑合集,按「现象 → 原因 → 解决」写,照着对基本能省半天。
4.1 启动报 UnsatisfiedLinkError
现象:一运行就抛java.lang.UnsatisfiedLinkError: Unable to load library 'tesseract'。原因:JVM 找不到对应平台的原生库,常见于 Linux 服务器或 M1 Mac。解决:确认 Tess4J 版本是否带当前平台库;Linux 上装系统级tesseract-ocr和libleptonica,或把.so放到java.library.path能扫到的目录,启动时加-Djava.library.path=/path/to/libs。
4.2 识别结果全是空或乱码
现象:doOCR不报错,但返回空串或一堆符号。原因:tessdata路径不对,或语言包版本与引擎不匹配。解决:先用tesseract.setDatapath指向绝对路径验证;再确认chi_sim.traineddata是 5.x 版本;最后检查图片本身是不是透明背景或极低对比度,必要时先做灰度增强。
4.3 中文识别率明显偏低
现象:英文数字还行,中文错得离谱。原因:只加载了eng,或图片分辨率太低。解决:setLanguage("chi_sim")或eng+chi_sim;把图片短边缩放到 1000 像素以上再送识别;中文场景下PSM_AUTO通常比SINGLE_LINE稳,别一刀切。
4.4 批量处理时内存持续上涨
现象:循环识别几百张图后 OOM。原因:每次new Tesseract()都会加载语言模型,开销不小。解决:复用同一个Tesseract实例,doOCR本身是线程不安全的,多线程场景下用ThreadLocal或对象池,别图省事每张图新建一个。
4.5 Windows 下路径带空格直接失败
现象:setDatapath("C:\\Program Files\\...")后识别异常。原因:部分版本对含空格路径处理不干净。解决:把tessdata放到无空格路径,比如项目根目录或D:\\tessdata,这是最省心的做法。
5. 二次开发:把 Demo 改成能进生产的 OCR 服务
Demo 跑通只是起点,真正要落地得考虑封装、并发和结果校验。这一章讲我怎么把它往可用方向推。
5.1 封装成可复用的识别组件
第一步是把Tesseract的创建和配置收进一个单例或工厂,避免散落在各处。我一般会写一个OcrService,构造时加载配置,对外只暴露recognize(BufferedImage)。
public class OcrService { private static final ThreadLocal<Tesseract> HOLDER = ThreadLocal.withInitial(() -> { Tesseract t = new Tesseract(); t.setDatapath("/opt/app/tessdata"); t.setLanguage("eng+chi_sim"); t.setPageSegMode(ITessAPI.TessPageSegMode.PSM_AUTO); return t; }); public String recognize(BufferedImage image) { try { return HOLDER.get().doOCR(image); } catch (TesseractException e) { throw new RuntimeException("OCR failed", e); } } }ThreadLocal保证每个线程一份实例,既复用了语言模型,又规避了线程安全问题。参数上datapath建议走配置中心或环境变量,别硬编码,部署到不同机器时改起来方便。
5.2 加一层结果校验与重试
OCR 不可能 100% 准,关键是让错误可控。我的做法是对关键字段做正则校验,不通过就换PSM重试一次,再不行才落人工。
| 场景 | 校验方式 | 重试策略 |
|---|---|---|
| 纯数字编号 | 正则^\d{8,20}$ | 换PSM_SINGLE_LINE重试 |
| 金额 | 正则^\d+\.\d{2}$ | 放大 1.5 倍后重试 |
| 中英混排文本 | 长度与字符集检查 | 不重试,直接标记低置信 |
这张表是我项目里实际用的简化版,核心思路是「不同字段不同容忍度」,别用一个阈值卡所有场景。
5.3 性能与部署上的取舍
单张 A4 扫描件在普通服务器上识别耗时大概几百毫秒到一两秒,批量场景下并发度别开太高,Tesseract 本身吃 CPU。我一般按核数的一半配线程池,再配合队列削峰。部署时把tessdata打进镜像或挂载卷,别每次启动去下载。还有一点,日志里别把整段识别结果原样打出来,涉及业务数据时只记长度和置信度统计,这是合规上的基本习惯。
从那以后我每次接 OCR 需求,都会先把「语言包版本、图片 DPI、PSM 模式」这三项在最小 Demo 上验一遍再动业务代码,省得后面在几百行逻辑里找玄学问题。希望帮到你。
本文还有配套的精品资源,点击获取