简介: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 测试图像和 hocr、pdf 输出样例,便于直接运行与对照调试。目前已有 914 人学习下载。通过完整工程结构与多格式图像素材,读者可快速掌握 Tess4J 配置流程、识别效果优化思路与常见异常处理方式,为构建可搜索文本提取应用打下基础。
1. 从一张模糊截图说起:Java Tess4J Demo 到底能干什么
上周同事甩过来一张手机拍的发票照片,让我把上面的发票号、金额、开票日期提取出来录进系统。我第一反应是手动敲,第二反应是——都什么年代了还手动敲。于是翻出了之前收藏的一个 Java Tess4J Demo,一个用 Java 封装 Tesseract-OCR 引擎的调用示例。它解决的就是这类问题:在 JVM 里直接对图片做 OCR,把图上的文字转成可编辑的字符串,不用装 Python 环境,不用调外部服务,一个 Maven 依赖加几行代码就能跑起来。
这个 Demo 适合谁?后端开发者需要在业务系统里集成图片文字识别能力,比如票据录入、证件信息提取、扫描件归档;也适合刚接触 OCR 的同学拿来理解 Tesseract 的调用链路。它不挑场景,但挑图片质量——这一点后面会反复提到。Tess4J 本质上是 Tesseract 的 JNI 封装,底层还是那个老牌开源 OCR 引擎,所以它的能力边界和 Tesseract 一致:印刷体识别够用,手写体和复杂版面就别指望了。
2. Tess4J 的调用链路:从 Maven 依赖到第一行识别结果
2.1 依赖引入与语言包准备
Tess4J 在 Maven 中央仓库可以直接拉到,但要注意版本和 Tesseract 本地库的匹配问题。我一般用 5.x 系列,配合 Tesseract 5 的引擎。依赖声明如下:
<dependency> <groupId>net.sourceforge.tess4j</groupId> <artifactId>tess4j</artifactId> <version>5.8.0</version> </dependency>这个依赖会自动带入 JNA 和几个平台相关的 native 库,但语言包(traineddata 文件)不会自动下载。你需要单独准备chi_sim.traineddata(简体中文)和eng.traineddata(英文),放到一个目录下,后面通过setDatapath指定。语言包从 Tesseract 的官方 tessdata 仓库获取,注意选对应版本的——5.x 引擎用 5.x 的语言包,混用会报Error opening data file或者识别结果乱码。
提示:语言包目录不要放在
src/main/resources里打包进 jar,因为 Tesseract 需要以文件路径方式访问,从 classpath 读流它不认。我一般放在项目根目录的tessdata/下,部署时单独挂载。
2.2 最小可运行识别代码
下面这段代码是我从 Demo 里抽出来的最小闭环,去掉所有花哨封装,就干一件事:加载图片、执行 OCR、输出文本。
import net.sourceforge.tess4j.Tesseract; import net.sourceforge.tess4j.TesseractException; import java.io.File; public class OcrDemo { public static void main(String[] args) { // 1. 创建 Tesseract 实例 Tesseract tesseract = new Tesseract(); // 2. 指定语言包所在目录(绝对路径或相对路径均可) tesseract.setDatapath("tessdata"); // 3. 设置识别语言:chi_sim 简体中文,eng 英文,用加号连接 tesseract.setLanguage("chi_sim+eng"); // 4. 可选:设置页面分割模式,3 表示全自动分割(默认) tesseract.setPageSegMode(3); // 5. 执行识别 try { String result = tesseract.doOCR(new File("invoice.jpg")); System.out.println(result); } catch (TesseractException e) { System.err.println("识别失败: " + e.getMessage()); } } }逻辑说明:setDatapath告诉 Tesseract 去哪里找.traineddata文件,路径结尾不要带斜杠;setLanguage用加号拼接多语言时,Tesseract 会依次尝试,但注意多语言混用会拖慢速度,如果图片里只有中文就别加eng。doOCR接收File或BufferedImage,返回识别出的纯文本,段落之间用换行分隔。
参数方面,setPageSegMode值得单独说。默认值 3 是「全自动页面分割」,适合大多数整页文档;如果你只识别图片中的一小块区域,用 6(统一文本块)或 7(单行文本)会更准。这个参数对结果影响很大,后面避坑章节会展开。
2.3 识别结果的置信度与后处理
doOCR只返回字符串,拿不到每个字的置信度。如果你需要过滤低质量结果,得用Tesseract的另一个方法getWords或者直接调TessAPI。Demo 里通常不涉及这层,但实际业务中我建议至少做一步后处理:去掉首尾空白、合并被错误换行拆散的字段、用正则提取关键信息。
// 简单后处理:按行读取,过滤空行,提取发票号 String raw = tesseract.doOCR(new File("invoice.jpg")); String[] lines = raw.split("\\r?\\n"); for (String line : lines) { String trimmed = line.trim(); if (trimmed.isEmpty()) continue; // 假设发票号是 8 位以上数字 if (trimmed.matches(".*\\d{8,}.*")) { System.out.println("候选发票号: " + trimmed); } }这段代码没什么玄学,就是逐行清洗。OCR 结果里常见的噪声包括:多余空格、把「0」识别成「O」、把「1」识别成「l」。如果业务对数字敏感,可以在正则里做字符替换映射,比如先把O替换成0再匹配。
3. 图像预处理:为什么原图直接丢进去十有八九翻车
3.1 灰度化、二值化与降噪的代码实现
Tesseract 对输入图像的质量非常敏感。手机拍的发票照片,光照不均、有阴影、背景杂乱,直接丢进去识别率可能不到 50%。我一般会先用 Java 的图像处理库做一轮预处理。Demo 里如果没带这部分,可以自己补上,用BufferedImage加ImageIO就能做基础操作。
import java.awt.image.BufferedImage; import java.awt.Color; import java.io.File; import javax.imageio.ImageIO; 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++) { Color c = new Color(src.getRGB(x, y)); int lum = (int) (0.299 * c.getRed() + 0.587 * c.getGreen() + 0.114 * c.getBlue()); gray.setRGB(x, y, new Color(lum, lum, lum).getRGB()); } } return gray; } // 简单二值化:阈值 128,高于变白,低于变黑 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 lum = new Color(gray.getRGB(x, y)).getRed(); bin.setRGB(x, y, lum > threshold ? Color.WHITE.getRGB() : Color.BLACK.getRGB()); } } return bin; } public static void main(String[] args) throws Exception { BufferedImage src = ImageIO.read(new File("invoice.jpg")); BufferedImage gray = toGray(src); BufferedImage bin = binarize(gray, 128); ImageIO.write(bin, "png", new File("invoice_bin.png")); } }逻辑说明:灰度化把三通道压成单通道,减少计算量;二值化把灰度图变成黑白图,让文字和背景的对比最大化。阈值 128 是经验值,如果图片偏暗,可以调到 100 甚至 80;偏亮则调到 150。更聪明的做法是用 Otsu 算法自动算阈值,但 Demo 级别手动调也够用。
3.2 缩放与DPI对识别率的影响
Tesseract 官方建议输入图像的字符高度在 30 到 40 像素之间。手机拍的照片往往分辨率很高,但字符实际占的像素可能只有十几。这时候直接识别,Tesseract 会「看不清」。解决办法是先把图片缩放到合适尺寸,或者提高 DPI 设置。
// 按比例缩放图片,目标宽度 2000px public static BufferedImage resize(BufferedImage src, int targetWidth) { int w = src.getWidth(), h = src.getHeight(); int targetHeight = (int) (h * (targetWidth / (double) w)); BufferedImage scaled = new BufferedImage(targetWidth, targetHeight, src.getType()); java.awt.Graphics2D g = scaled.createGraphics(); g.drawImage(src, 0, 0, targetWidth, targetHeight, null); g.dispose(); return scaled; }参数怎么定:如果原图宽度小于 1000px,放大到 1500 到 2000px 通常有提升;如果原图已经超过 3000px,反而要缩小,因为 Tesseract 处理超大图会慢且容易出错。我一般把宽度控制在 2000px 左右,字符高度落在 30 到 40px 区间。这个不是绝对标准,但按这个来,大部分票据类图片的识别率能明显改善。
注意:缩放用
Graphics2D的默认插值算法就够,不要用最近邻,否则边缘锯齿会干扰识别。如果追求更好效果,可以用双线性或双三次插值,但代码量会增加。
4. 避坑与排查:Tess4J 集成中最容易踩的五个坑
4.1 坑一:UnsatisfiedLinkError,native 库加载失败
现象:运行时报java.lang.UnsatisfiedLinkError: Unable to load library 'tesseract'或者libtesseract.so not found。
原因:Tess4J 依赖 JNA 加载本地 Tesseract 库,但不同平台的库文件不一样。Maven 依赖虽然带了部分平台的 native 库,但在某些 Linux 发行版或 macOS 新版本上,系统缺少依赖的共享库(比如liblept.so),或者架构不匹配(arm64 vs x86_64)。
解决:先确认tess4j的 jar 包里是否包含对应平台的 native 库,可以用jar tf查看。如果没有,需要手动安装 Tesseract 并设置jna.library.path指向系统库目录。Linux 上apt install tesseract-ocr libtesseract-dev通常能解决;macOS 用brew install tesseract。设置 JVM 参数-Djna.library.path=/usr/local/lib指定库路径。
4.2 坑二:识别结果全是乱码或空字符串
现象:doOCR返回空字符串,或者返回一堆无意义的符号。
原因:最常见的是语言包路径不对或语言包版本不匹配。setDatapath指向的目录下没有对应的.traineddata文件,或者文件名大小写不对(Linux 区分大小写)。另一个原因是图片本身质量太差,Tesseract 直接放弃了。
解决:先确认tessdata目录下有chi_sim.traineddata和eng.traineddata,文件名全小写。然后在代码里打印tesseract.getDatapath()和tesseract.getLanguage()确认设置生效。如果路径没问题,换一张清晰的测试图验证引擎本身是否正常。最后再排查图片质量,按第 3 章的预处理流程走一遍。
4.3 坑三:中文识别率极低,英文却正常
现象:同一张图,英文部分识别准确,中文部分错字连篇。
原因:Tesseract 的中文模型对字体和排版比较挑剔。默认的chi_sim模型是基于印刷体训练的,对宋体、黑体效果尚可,但对楷体、手写体、艺术字基本无能为力。另外,中文没有空格分隔,Tesseract 的分词逻辑容易把相邻的字合并或拆错。
解决:优先用印刷体清晰的图片。如果必须识别特殊字体,可以考虑用chi_sim_vert(竖排)或者训练自己的模型,但训练成本很高,Demo 级别不建议。实际项目中,我一般把中文识别和英文识别分开跑,中文用chi_sim,英文用eng,然后按位置合并结果,比混用一个语言包准确率更高。
4.4 坑四:多线程调用时结果错乱或崩溃
现象:在 Web 应用里并发调用doOCR,偶尔返回其他请求的识别结果,或者直接 JVM 崩溃。
原因:Tesseract实例不是线程安全的。Tess4J 内部通过 JNA 调用 native 库,native 层有全局状态。多个线程共用一个Tesseract实例时,状态会互相污染。
解决:每个线程创建独立的Tesseract实例,或者用ThreadLocal做线程绑定。不要用单例。如果并发量高,建议用对象池管理Tesseract实例,避免频繁创建销毁的开销。另外,doOCR是阻塞调用,Web 场景下要放到独立线程池里执行,不要占用请求线程。
4.5 坑五:内存泄漏与临时文件堆积
现象:长时间运行后,/tmp目录下出现大量tess4j_*临时文件,内存占用持续上升。
原因:Tess4J 在识别过程中会生成临时文件(比如中间图像),如果 JVM 异常退出或者流没关闭,这些文件不会被清理。另外,Tesseract实例如果不显式释放,native 层的内存不会回收。
解决:在finally块里调用tesseract.dispose()释放 native 资源。对于临时文件,可以设置系统属性java.io.tmpdir指向一个专用目录,定期清理。如果用的是 5.x 版本,检查是否有已知的临时文件泄漏 issue,升级到最新小版本通常有修复。
5. 进阶技巧:用 ROI 裁剪和结果校验把识别率再拉高一截
5.1 按区域识别代替全图识别
全图识别最大的问题是干扰太多。发票上有表格线、印章、二维码、背景图案,Tesseract 的分割算法很容易被这些元素带偏。我的做法是先用 OpenCV 或者简单的颜色阈值定位关键区域,裁剪出来再单独识别。Java 里可以用BufferedImage.getSubimage做裁剪。
// 假设已知发票号在图片的某个矩形区域内 BufferedImage full = ImageIO.read(new File("invoice.jpg")); // 裁剪区域:x, y, width, height BufferedImage roi = full.getSubimage(120, 340, 400, 60); // 对 ROI 做预处理后识别 BufferedImage grayRoi = ImagePreprocess.toGray(roi); BufferedImage binRoi = ImagePreprocess.binarize(grayRoi, 128); String result = tesseract.doOCR(binRoi); System.out.println("发票号区域识别结果: " + result);逻辑说明:getSubimage的参数是相对于原图左上角的像素坐标。ROI 的坐标怎么来?如果是固定版式的票据,可以硬编码;如果版式不固定,需要先做模板匹配或轮廓检测。Demo 里通常不涉及自动定位,但实际业务中这一步是提升准确率的关键。裁剪之后,Tesseract 只需要处理一小块干净区域,干扰少了,准确率自然上去。
5.2 用白名单和字符集约束输出
Tesseract 支持设置字符白名单,告诉它「只可能输出这些字符」。比如发票号只包含数字和大写字母,就可以把识别范围缩小,减少误识别。
// 设置白名单:只识别数字和常见大写字母 tesseract.setVariable("tessedit_char_whitelist", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"); // 识别 String result = tesseract.doOCR(roi);这个参数对数字类字段效果非常明显。之前识别发票号,不加白名单时经常把0识别成O、1识别成I,加了白名单之后这些错误基本消失。注意白名单是全局设置,设置后对所有后续识别生效,如果同一实例要识别不同字段,记得在每次调用前重设。
5.3 结果校验:用正则和校验位兜底
OCR 不可能 100% 准确,所以最后一步一定要做校验。发票号有固定位数和校验规则,身份证号有校验位,金额有格式要求。用正则先过滤掉明显不对的结果,再用校验算法做二次确认。
// 发票号校验:假设是 8 位数字,最后一位是校验位 public static boolean validateInvoiceNo(String no) { if (!no.matches("\\d{8}")) return false; int sum = 0; for (int i = 0; i < 7; i++) { sum += (no.charAt(i) - '0') * (i + 1); } return sum % 10 == (no.charAt(7) - '0'); }这段代码是示意,实际校验规则按业务来。核心思路是:OCR 输出不可全信,必须有一层确定性校验。校验不通过就触发人工复核或者重新识别,不要直接把 OCR 结果写进数据库。
从那以后我每次集成 OCR 都会强制走一遍「预处理 → ROI 裁剪 → 白名单 → 校验」的流程,少一步心里都不踏实。希望帮到你。
本文还有配套的精品资源,点击获取