Spring Boot 深度集成 Tess4J 实战:构建企业级 OCR 服务

# Spring Boot 深度集成 Tess4J 实战:构建企业级 OCR 服务

# 引言

在企业级应用开发中,OCR(Optical Character Recognition,光学字符识别)技术扮演着越来越重要的角色。从发票处理、文档数字化到身份验证,OCR 技术正在帮助企业实现业务流程的自动化和数字化转型。

对于 Java 生态系统而言,Tess4J 是一个不可忽视的选择。它是 Tesseract OCR 引擎的纯 Java 实现,无需额外的原生库支持即可在 Java 应用程序中无缝运行。本文将深入探讨如何在 Spring Boot 框架中深度集成 Tess4J,从技术原理到最佳实践,从基础功能到性能优化,为开发者提供一份完整的实战指南。

# Tess4J 技术概述

# 什么是 Tess4J

Tess4J 是 Tesseract OCR 引擎的 Java 客户端库,由 Nguyen Giang Thanh 开发和维护。它将强大的 Tesseract 引擎包装为纯 Java 类库,通过 JNA(Java Native Access)直接调用 Tesseract 的 C/C++ 原生代码。这种设计既保留了 Tesseract 高精度识别的核心优势,又提供了 Java 开发者熟悉的编程接口。

Tesseract 本身经历了数十年的发展和迭代,目前由 Google 维护。作为开源世界最成熟的 OCR 引擎之一,Tesseract 支持超过 100 种语言,拥有活跃的社区和丰富的文档资源。Tess4J 则让这一切触手可及,让 Java 开发者无需编写 JNI 胶水代码即可直接享受 Tesseract 的强大能力。

# 核心架构解析

Tess4J 的核心架构可以分为三个层次:

Java API 层:这是开发者直接交互的接口层,包括 ITesseract 接口和 Tesseract 实现类。开发者通过这些类提供图像输入,配置识别参数,并获取识别结果。

JNA 桥接层:负责 Java 与原生 Tesseract 库之间的通信。JNA 相比传统的 JNI 大幅简化了原生代码的调用过程,开发者无需编写 C/C++ 桥接代码,只需在 Java 中声明原生方法即可。

Tesseract 引擎层:这是实际执行 OCR 运算的核心引擎,包含图像预处理、字符分割、特征提取、模板匹配等算法模块。引擎支持多种配置模式,包括不同的页面分割模式(PSM)和 OCR 引擎模式(OEM)。

# 支持的图像格式与语言

Tess4J 支持丰富的图像格式,涵盖了主流的位图格式:

  • TIFF(支持单页和多页)
  • JPEG/JPEG2000
  • PNG
  • BMP
  • GIF
  • PCX
  • WebP

语言支持方面,Tess4J 基于 Tesseract 的语言包机制,默认支持英语,并可以通过下载额外的语言数据文件支持超过 100 种语言,包括简体中文(chi_sim)、繁体中文(chi_tra)、日语(jpn)、韩语(kor)、阿拉伯语(ara)、俄语(rus)等。

# Tess4J 优势分析

# 纯 Java API,跨平台部署

Tess4J 提供了纯 Java 编程接口,开发者可以使用熟悉的 Java 方式调用 OCR 功能。然而,需要特别说明的是:Tess4J 底层仍然依赖 Tesseract 原生库和 Leptonica 图像处理库

不同平台的依赖情况如下:

平台 是否需要手动安装 说明
Windows Tess4J 发行包已自带 DLL 文件,开箱即用
Linux ✅ 是 需要通过系统包管理器安装 tesseract-ocr
macOS ✅ 是 需要通过 Homebrew 安装 tesseract

关键提醒:无论哪个平台,都必须准备语言数据文件.traineddata),这些文件需要单独下载并放置在指定目录。

这种设计的优势在于:

  • 开发体验一致:只需引入 Maven 依赖即可开始编码
  • 语言数据独立:语言包与代码分离,便于更新和扩展
  • 跨平台兼容:不同平台只需配置对应的原生库,无需修改代码

# 开源免费,商业友好

作为 Apache 2.0 许可证下的开源项目,Tess4J 可以免费用于商业项目,没有任何授权费用。这一特性对于需要控制成本的企业项目尤为重要。相比动辄数千美元的商用 OCR SDK,Tess4J 提供了一个零成本的高质量替代方案。

同时,开源意味着开发者可以深入了解其实现细节,在遇到问题时可以通过阅读源码进行排查和修复。对于有定制需求的团队,也可以基于 Tess4J 进行二次开发。

# 成熟稳定,社区活跃

Tesseract 项目始于 1985 年,至今已有近 40 年的历史。这么长时间的迭代使其成为最稳定、最可靠的 OCR 引擎之一。Tess4J 作为其 Java 封装,也继承了这种稳定性。

社区活跃度也是选择技术栈时的重要考量。Tess4J 在 GitHub 上拥有持续的更新,Stack Overflow 上有丰富的问答资源,GitHub Issues 中问题能够得到及时响应。这些都是一个技术方案能否在生产环境中长期使用的关键保障。

# 灵活的配置选项

Tess4J 提供了丰富的配置选项,开发者可以根据具体场景进行深度优化:

页面分割模式(PSM):Tesseract 支持多种页面分割模式,从自动检测文本区域到强制单字符识别,共有 11 种模式可选。这种灵活性使得 Tess4J 能够适应不同类型的文档。

OCR 引擎模式(OEM):支持使用传统识别引擎、LSTM 神经网络引擎或两者的组合。LSTM 引擎在大多数场景下能够提供更高的识别准确率。

语言与训练数据:可以针对特定语言或领域训练自定义数据,进一步提升识别效果。

# 与 Spring 生态无缝集成

对于已经使用 Spring Boot 构建后端服务的团队,Tess4J 可以轻松集成到现有的技术栈中。它不引入额外的复杂性,不需要特殊的环境配置,可以像使用其他 Java 库一样的方式引入项目。

# Tess4J 局限性考量

# 识别准确率不如商业方案

尽管 Tesseract 在开源 OCR 引擎中表现出色,但与商用 OCR 解决方案(如 ABBYY、Adobe Acrobat Pro)相比,识别准确率仍有差距。特别是在以下场景中:

复杂布局文档:对于多栏排版、表格密集、图像与文字混合的复杂文档,Tesseract 的版面分析能力有限。

手写体识别:Tesseract 主要针对印刷体优化,对手写体的识别能力较弱。

低质量图像:对于严重倾斜、模糊、噪点多的图像,识别效果会明显下降。

特殊字体:使用艺术字体或非标准字体时,识别错误率会上升。

# 性能表现中等

由于 Tesseract 最初并非为高性能场景设计,Tess4J 在处理大量图像时可能面临性能挑战:

首次加载慢:Tesseract 引擎在首次初始化时需要加载语言数据和神经网络模型,这一过程可能需要数秒钟。

单线程处理:默认情况下,Tesseract 以单线程模式运行,处理速度取决于 CPU 性能。

内存占用:加载完整的语言包和模型文件会占用较多内存,在资源受限的环境中需要特别注意。

# 对图像预处理依赖度高

Tess4J 的识别效果高度依赖输入图像的质量。如果不进行适当的图像预处理,直接对原始照片或扫描件进行识别,往往难以获得理想的结果。这意味着在实际应用中,开发者需要额外实现图像预处理逻辑,包括灰度化、二值化、去噪、倾斜校正等。

# 不支持实时视频流处理

Tess4J 设计用于处理静态图像,不适合实时视频流场景。如果需要从视频中提取文字(如车牌识别、视频字幕提取),需要选择其他技术方案。

# 典型应用场景

# 发票与财务文档处理

在财务领域,发票的数字化是一个常见需求。通过 Tess4J,可以实现发票信息的自动提取,包括发票号码、日期、金额、商品明细等字段。这大大减少了人工录入的工作量,提高了财务处理效率。

典型的处理流程包括:扫描或拍照获取发票图像 → 图像预处理(倾斜校正、增强对比度) → Tess4J 识别文字 → 关键信息提取与结构化 → 存入系统或触发后续流程。

# 合同与文档数字化

企业日常运营中会产生大量纸质合同、协议、证明文件。通过 OCR 技术将这些文档数字化,可以实现全文搜索、归档备份、数据分析等功能。Tess4J 特别适合处理大量的标准格式文档,如标准合同、格式化表格等。

# 身份证明文件识别

在金融、政务、在线教育等场景中,经常需要验证用户提交的身份证明文件。通过 Tess4J 识别身份证、营业执照、驾驶证等证件,可以自动提取关键字段进行核验或录入系统。

# 图书与资料数字化

图书馆、档案馆、学校等机构拥有大量纸质藏书和资料,需要进行数字化保存和检索。Tess4J 可以批量处理扫描的图书页面,将纸质内容转换为可搜索的文本数据。

# 生产制造追溯

在制造业中,产品标签、序列号、规格参数等信息通常以文本形式印在产品或包装上。通过视觉系统采集图像后使用 Tess4J 识别,可以实现生产追溯、质量控制、自动入库等功能的自动化。

# 教育考试评分

在教育场景中,Tess4J 可以用于客观题答题卡的自动阅卷。将答题卡扫描后识别涂点位置,自动计算分数,大幅提升评卷效率。

# 环境配置与安装指南

在使用 Tess4J 之前,需要根据不同的操作系统完成环境配置。本节将详细介绍各平台的具体安装步骤。

# Windows 环境配置

# 方式一:使用 Maven 发行版(推荐开发环境)

Tess4J 的 Maven 发行版已经包含了 Windows 所需的原生 DLL 文件,开发阶段可以直接使用:

  1. 下载语言数据文件

访问 Tesseract 官方 GitHub 仓库或其他镜像站点下载语言包:

# 创建 tessdata 目录
mkdir -p src/main/resources/tessdata

# 下载语言包(以中文简体为例)
# 英文(必需)
wget https://github.com/tesseract-ocr/tessdata/raw/main/eng.traineddata \
  -O src/main/resources/tessdata/eng.traineddata

# 中文简体
wget https://github.com/tesseract-ocr/tessdata/raw/main/chi_sim.traineddata \
  -O src/main/resources/tessdata/chi_sim.traineddata
1
2
3
4
5
6
7
8
9
10
11
  1. 添加 Maven 依赖
<dependency>
    <groupId>net.sourceforge.tess4j</groupId>
    <artifactId>tess4j</artifactId>
    <version>5.5.0</version>
</dependency>
1
2
3
4
5
  1. 验证安装

创建测试类验证配置是否正确:

public class Tess4JTest {
    public static void main(String[] args) {
        Tesseract tesseract = new Tesseract();
        tesseract.setDatapath("src/main/resources/tessdata");
        tesseract.setLanguage("chi_sim+eng");
        
        try {
            String result = tesseract.doOCR(new File("test.png"));
            System.out.println("识别结果: " + result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 方式二:手动安装 Tesseract(推荐生产环境)

生产环境建议手动安装 Tesseract 以获得更好的性能和更多配置选项:

  1. 下载 Tesseract Windows 安装包

访问以下地址下载 Windows 安装包:

  • 官方地址:https://github.com/UB-Mannheim/tesseract/wiki
  • 32位版本:tesseract-ocr-w32-setup-5.3.1.20230401.exe
  • 64位版本:tesseract-ocr-w64-setup-5.3.1.20230401.exe
  1. 安装 Tesseract

运行安装程序,记住安装路径(假设为 C:\Program Files\Tesseract-OCR

  1. 下载语言包

将语言包下载到 Tesseract 安装目录下的 tessdata 文件夹:

# 创建语言包目录
New-Item -ItemType Directory -Path "C:\Program Files\Tesseract-OCR\tessdata"

# 下载语言包(使用 PowerShell)
# 英文
Invoke-WebRequest -Uri "https://github.com/tesseract-ocr/tessdata/raw/main/eng.traineddata" `
  -OutFile "C:\Program Files\Tesseract-OCR\tessdata\eng.traineddata"

# 中文简体
Invoke-WebRequest -Uri "https://github.com/tesseract-ocr/tessdata/raw/main/chi_sim.traineddata" `
  -OutFile "C:\Program Files\Tesseract-OCR\tessdata\chi_sim.traineddata"
1
2
3
4
5
6
7
8
9
10
11
  1. 配置 Java 项目
Tesseract tesseract = new Tesseract();
// 使用安装路径
tesseract.setDatapath("C:\\Program Files\\Tesseract-OCR");
tesseract.setLanguage("chi_sim+eng");
1
2
3
4

# Linux 环境配置

# Ubuntu / Debian

# 1. 更新软件源
sudo apt-get update

# 2. 安装 Tesseract 及图形依赖
sudo apt-get install -y tesseract-ocr
sudo apt-get install -y libtesseract-dev libleptonica-dev

# 3. 安装语言包(以中文简体为例)
sudo apt-get install -y tesseract-ocr-chi-sim

# 4. 验证安装
tesseract --version

# 5. 查看已安装的语言包
ls /usr/share/tesseract-*/tessdata/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

语言包完整列表

# 查看可用语言包
apt-cache search tesseract-ocr

# 安装常用语言
sudo apt-get install -y tesseract-ocr-eng      # 英语
sudo apt-get install -y tesseract-ocr-chi-sim  # 简体中文
sudo apt-get install -y tesseract-ocr-chi-tra  # 繁体中文
sudo apt-get install -y tesseract-ocr-jpn      # 日语
sudo apt-get install -y tesseract-ocr-kor      # 韩语
sudo apt-get install -y tesseract-ocr-deu      # 德语
sudo apt-get install -y tesseract-ocr-fra      # 法语
1
2
3
4
5
6
7
8
9
10
11

# CentOS / RHEL / Fedora

# 1. 安装 EPEL 仓库(如果需要)
sudo yum install -y epel-release

# 2. 安装 Tesseract
sudo yum install -y tesseract

# 3. 安装语言包
sudo yum install -y tesseract-lang

# 4. 验证安装
tesseract --version
1
2
3
4
5
6
7
8
9
10
11

# 自定义语言包路径

如果使用非系统路径的语言包:

# 创建自定义目录
mkdir -p ~/tessdata

# 下载语言包
cd ~/tessdata
wget https://github.com/tesseract-ocr/tessdata/raw/main/eng.traineddata
wget https://github.com/tesseract-ocr/tessdata/raw/main/chi_sim.traineddata
1
2
3
4
5
6
7

在 Java 代码中指定路径:

tesseract.setDatapath(System.getProperty("user.home") + "/tessdata");
1

# macOS 环境配置

# 使用 Homebrew(推荐)

# 1. 安装 Homebrew(如果未安装)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# 2. 安装 Tesseract
brew install tesseract

# 3. 安装语言包
brew install tesseract-lang

# 4. 验证安装
tesseract --version
# 输出示例:
# tesseract 5.3.1
#  leptonica-1.83.1
#    libgif 5.2.1 : libjpeg 8d (libjpeg-turbo 1.5.4) : libpng 1.6.40 : libtiff 4.5.1 : zlib 1.2.13 : libwebp 1.3.1 : libopenjp2 2.5.0

# 5. 查看语言包位置
ls $(brew --prefix)/share/tessdata/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 使用 MacPorts

# 安装 MacPorts(如果未安装)
# 下载地址:https://www.macports.org/install.php

sudo port install tesseract
sudo port install tesseract-lang
1
2
3
4
5

# 配置 Java 项目

// macOS 上获取 Tesseract 路径
String tesseractPath = "/opt/homebrew/share/tesseract";  // Homebrew 默认路径

// 或通过命令获取
// brew --prefix tesseract

tesseract.setDatapath(tesseractPath);
tesseract.setLanguage("chi_sim+eng");
1
2
3
4
5
6
7
8

# Docker 环境配置

# 基于 Ubuntu 的 Dockerfile

FROM maven:3.9-eclipse-temurin-21 AS builder

# 复制源码
COPY pom.xml .
COPY src ./src

# 构建项目
RUN mvn clean package -DskipTests

# 运行镜像
FROM eclipse-temurin:21-jre

# 安装 Tesseract 和语言包
RUN apt-get update && apt-get install -y \
    tesseract-ocr \
    tesseract-ocr-chi-sim \
    tesseract-ocr-eng \
    && rm -rf /var/lib/apt/lists/*

# 复制构建产物
COPY --from=builder target/ocr-service-*.jar app.jar

# 复制语言包
COPY --from=builder target/classes/tessdata /app/tessdata

ENV JAVA_OPTS="-Xms512m -Xmx1024m"

EXPOSE 8080

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Docr.tessdata.path=/app/tessdata -jar app.jar"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 基于 Alpine 的轻量镜像

FROM eclipse-temurin:21-jre-alpine

# 安装 Tesseract(Alpine 仓库)
RUN apk add --no-cache tesseract-ocr tesseract-ocr-data-chi_sim tesseract-ocr-data-eng

# 复制应用
COPY target/ocr-service-*.jar app.jar

# Alpine 镜像中 tessdata 位于 /usr/share/tesseract-ocr/tessdata
ENV TESSDATA_PREFIX=/usr/share/tesseract-ocr

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 语言数据文件详解

# 常用语言代码

语言 代码 文件大小(估算)
英语 eng 2 MB
简体中文 chi_sim 9 MB
繁体中文 chi_tra 9 MB
日语 jpn 7 MB
韩语 kor 6 MB
德语 deu 3 MB
法语 fra 3 MB
西班牙语 spa 3 MB
俄语 rus 3 MB
阿拉伯语 ara 2 MB

# 多语言组合配置

Tesseract 支持多语言同时识别,配置方式如下:

// 同时识别英文和中文简体
tesseract.setLanguage("chi_sim+eng");

// 三个语言组合
tesseract.setLanguage("chi_sim+eng+jpn");

// 指定语言优先级(第一个语言为主)
tesseract.setLanguage("eng+chi_sim");
1
2
3
4
5
6
7
8

# 语言包下载脚本

创建一个便捷的下载脚本:

#!/bin/bash

# download-tessdata.sh

TESSDATA_DIR=${1:-"./tessdata"}
mkdir -p "$TESSDATA_DIR"

# 语言包列表
LANGUAGES=(
    "eng"
    "chi_sim"
    "chi_tra"
    "jpn"
    "kor"
    "deu"
    "fra"
    "spa"
    "rus"
    "ara"
)

# 镜像源(使用国内源加速)
MIRRORS=(
    "https://github.com/tesseract-ocr/tessdata/raw/main"
    "https://gitee.com/mirrors/tessdata/raw/main"
)

download_file() {
    local lang=$1
    local url=$2
    
    local filepath="$TESSDATA_DIR/${lang}.traineddata"
    
    if [ -f "$filepath" ]; then
        echo "[SKIP] $lang (already exists)"
        return
    fi
    
    echo "[DOWNLOAD] $lang from $url"
    if curl -L -o "$filepath" "$url/${lang}.traineddata" 2>/dev/null; then
        echo "[OK] $lang"
    else
        echo "[FAIL] $lang"
        rm -f "$filepath"
    fi
}

for lang in "${LANGUAGES[@]}"; do
    downloaded=false
    
    # 尝试多个镜像
    for mirror in "${MIRRORS[@]}"; do
        if [ ! -f "$TESSDATA_DIR/${lang}.traineddata" ]; then
            download_file "$lang" "$mirror"
        fi
    done
done

echo ""
echo "下载完成,语言包位于: $TESSDATA_DIR"
ls -lh "$TESSDATA_DIR"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

使用方式:

chmod +x download-tessdata.sh
./download-tessdata.sh ./tessdata
1
2

# 常见问题排查

# 问题一:UnsatisfiedLinkError

错误信息

java.lang UnsatisfiedLinkError: Unable to load library 'tesseract': The specified procedure could not be found.
1

解决方案

  1. 确认 Tesseract 原生库已正确安装
  2. 检查 Java 架构(32位/64位)是否匹配
  3. Windows 上确保 DLL 文件在系统 PATH 中或指定正确路径

# 问题二:TessdataNotFoundException

错误信息

net.sourceforge.tess4j.TessdataNotFoundException: tessdata not found
1

解决方案

  1. 确认语言包文件(.traineddata)已下载
  2. 检查 setDatapath() 指定的路径是否正确
  3. 路径中避免中文和特殊字符

# 问题三:语言包版本不匹配

错误信息

TessException: Failed to load language eng
1

解决方案

  1. 重新下载与 Tesseract 版本匹配的语言包
  2. 确保语言包文件完整(下载未完成会导致此错误)

# 问题四:OOM(内存溢出)

解决方案

  1. 降低图像分辨率后再处理
  2. 增大 JVM 堆内存:-Xmx2g
  3. 使用流式处理,避免一次性加载大量图像

# Spring Boot 集成实战

# 项目初始化与依赖配置

首先创建 Spring Boot 项目并添加 Tess4J 依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.2</version>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>ocr-service</artifactId>
    <version>1.0.0</version>
    
    <properties>
        <java.version>17</java.version>
        <tess4j.version>5.5.0</tess4j.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Tess4J OCR -->
        <dependency>
            <groupId>net.sourceforge.tess4j</groupId>
            <artifactId>tess4j</artifactId>
            <version>${tess4j.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>com.sun.jna</groupId>
                    <artifactId>jna</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        
        <!-- 显式指定 JNA 版本 -->
        <dependency>
            <groupId>com.sun.jna</groupId>
            <artifactId>jna</artifactId>
            <version>5.14.0</version>
        </dependency>
        
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

# 配置文件

application.yml 中配置 OCR 服务参数:

ocr:
  tessdata:
    path: classpath:tessdata
    language: chi_sim+eng
  engine:
    mode: LSTM_ONLY
    psm: AUTO
  performance:
    timeout-seconds: 60
    cache-enabled: true
  image:
    dpi: 300
    scale-factor: 2.0

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# OCR 服务核心实现

# OCR 结果模型

package com.example.ocr.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OcrResult {
    
    /**
     * 识别状态
     */
    private boolean success;
    
    /**
     * 识别的完整文本
     */
    private String text;
    
    /**
     * 置信度 (0-100)
     */
    private Double confidence;
    
    /**
     * 详细结果(包含每个文本块的信息)
     */
    private List<TextBlock> blocks;
    
    /**
     * 错误信息
     */
    private String errorMessage;
    
    /**
     * 处理耗时(毫秒)
     */
    private Long processTime;
    
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class TextBlock {
        /**
         * 文本内容
         */
        private String text;
        
        /**
         * 置信度
         */
        private Double confidence;
        
        /**
         * 所在页面(多页文档时)
         */
        private int pageNum;
        
        /**
         * 段落索引
         */
        private int paragraphNum;
        
        /**
         * 文本行索引
         */
        private int lineNum;
        
        /**
         * 单词索引
         */
        private int wordNum;
        
        /**
         * 边界框信息
         */
        private BoundingBox boundingBox;
    }
    
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class BoundingBox {
        private int x1, y1;
        private int x2, y2;
        private int x3, y3;
        private int x4, y4;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

# 图像预处理工具类

package com.example.ocr.util;

import net.sourceforge.tess4j.ITessAPI;
import org.springframework.stereotype.Component;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * 图像预处理工具类
 * 良好的图像预处理是提高 OCR 识别准确率的关键
 */
@Component
public class ImagePreprocessor {
    
    /**
     * 灰度化处理
     */
    public BufferedImage toGrayscale(BufferedImage image) {
        int width = image.getWidth();
        int height = image.getHeight();
        BufferedImage gray = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
        
        Graphics2D g = gray.createGraphics();
        g.drawImage(image, 0, 0, null);
        g.dispose();
        
        return gray;
    }
    
    /**
     * 二值化处理(Otsu's 方法自动阈值)
     */
    public BufferedImage binarize(BufferedImage image) {
        int width = image.getWidth();
        int height = image.getHeight();
        
        BufferedImage binary = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY);
        
        // 转换为灰度图像进行计算
        BufferedImage gray = toGrayscale(image);
        
        // 计算 Otsu 阈值
        int threshold = calculateOtsuThreshold(gray);
        
        Graphics2D g = binary.createGraphics();
        g.drawImage(gray, 0, 0, null);
        
        // 应用阈值
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int pixel = gray.getRaster().getSample(x, y, 0);
                binary.setRGB(x, y, pixel > threshold ? Color.WHITE.getRGB() : Color.BLACK.getRGB());
            }
        }
        
        g.dispose();
        return binary;
    }
    
    /**
     * 计算 Otsu 阈值
     */
    private int calculateOtsuThreshold(BufferedImage image) {
        int[] histogram = new int[256];
        int width = image.getWidth();
        int height = image.getHeight();
        
        // 计算直方图
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int pixel = image.getRaster().getSample(x, y, 0);
                histogram[pixel]++;
            }
        }
        
        int total = width * height;
        float sum = 0;
        for (int i = 0; i < 256; i++) {
            sum += i * histogram[i];
        }
        
        float sumB = 0;
        int wB = 0;
        float maxVariance = 0;
        int threshold = 0;
        
        for (int t = 0; t < 256; t++) {
            wB += histogram[t];
            if (wB == 0) continue;
            
            int wF = total - wB;
            if (wF == 0) break;
            
            sumB += t * histogram[t];
            float mB = sumB / wB;
            float mF = (sum - sumB) / wF;
            
            float variance = wB * wF * (mB - mF) * (mB - mF);
            
            if (variance > maxVariance) {
                maxVariance = variance;
                threshold = t;
            }
        }
        
        return threshold;
    }
    
    /**
     * 倾斜校正
     */
    public BufferedImage deskew(BufferedImage image) {
        // 简化实现:返回原图
        // 实际项目中可以使用更复杂的算法检测文本倾斜角度并进行旋转
        return image;
    }
    
    /**
     * 降噪处理
     */
    public BufferedImage denoise(BufferedImage image) {
        int width = image.getWidth();
        int height = image.getHeight();
        BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
        
        Graphics2D g = result.createGraphics();
        g.drawImage(image, 0, 0, null);
        
        // 简单的中值滤波降噪
        int[] pixels = new int[width * height];
        result.getRGB(0, 0, width, height, pixels, 0, width);
        
        for (int y = 1; y < height - 1; y++) {
            for (int x = 1; x < width - 1; x++) {
                int[] neighborhood = {
                    pixels[(y - 1) * width + (x - 1)],
                    pixels[(y - 1) * width + x],
                    pixels[(y - 1) * width + (x + 1)],
                    pixels[y * width + (x - 1)],
                    pixels[y * width + x],
                    pixels[y * width + (x + 1)],
                    pixels[(y + 1) * width + (x - 1)],
                    pixels[(y + 1) * width + x],
                    pixels[(y + 1) * width + (x + 1)]
                };
                
                // 排序并取中值
                java.util.Arrays.sort(neighborhood);
                pixels[y * width + x] = neighborhood[4];
            }
        }
        
        result.setRGB(0, 0, width, height, pixels, 0, width);
        g.dispose();
        
        return result;
    }
    
    /**
     * 对比度增强
     */
    public BufferedImage enhanceContrast(BufferedImage image, double factor) {
        int width = image.getWidth();
        int height = image.getHeight();
        BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
        
        Graphics2D g = result.createGraphics();
        g.drawImage(image, 0, 0, null);
        
        // 简单的对比度增强
        RescaleOp op = new RescaleOp((float) factor, 0, null);
        op.filter(image, result);
        
        g.dispose();
        return result;
    }
    
    /**
     * 调整图像分辨率(DPI)
     */
    public BufferedImage setDPI(BufferedImage image, int dpi) {
        // DPI 影响 OCR 识别精度,通常 300 DPI 是较好的选择
        // 这里不需要实际修改图像,只需在读取时指定正确的 DPI
        return image;
    }
    
    /**
     * 缩放图像
     */
    public BufferedImage scale(BufferedImage image, double scaleFactor) {
        int newWidth = (int) (image.getWidth() * scaleFactor);
        int newHeight = (int) (image.getHeight() * scaleFactor);
        
        BufferedImage scaled = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_BYTE_GRAY);
        Graphics2D g = scaled.createGraphics();
        
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.drawImage(image, 0, 0, newWidth, newHeight, null);
        g.dispose();
        
        return scaled;
    }
    
    /**
     * 预处理流程(推荐的标准流程)
     */
    public BufferedImage preprocess(BufferedImage image, OcrConfig config) {
        // 1. 灰度化
        BufferedImage processed = toGrayscale(image);
        
        // 2. 缩放(如果需要)
        if (config.getScaleFactor() != null && config.getScaleFactor() > 1) {
            processed = scale(processed, config.getScaleFactor());
        }
        
        // 3. 对比度增强
        processed = enhanceContrast(processed, 1.2);
        
        // 4. 二值化
        processed = binarize(processed);
        
        // 5. 降噪
        processed = denoise(processed);
        
        // 6. 倾斜校正
        processed = deskew(processed);
        
        return processed;
    }
    
    /**
     * 从字节数组加载图像
     */
    public BufferedImage loadImage(byte[] imageData) throws IOException {
        ByteArrayInputStream bais = new ByteArrayInputStream(imageData);
        return ImageIO.read(bais);
    }
    
    /**
     * 将图像转换为字节数组
     */
    public byte[] toBytes(BufferedImage image, String format) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(image, format, baos);
        return baos.toByteArray();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252

# OCR 配置类

package com.example.ocr.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "ocr")
public class OcrConfig {
    
    /**
     * tessdata 文件路径
     */
    private String path = "classpath:tessdata";
    
    /**
     * 识别语言(支持多语言组合,如 "chi_sim+eng")
     */
    private String language = "eng";
    
    /**
     * OCR 引擎模式
     */
    private EngineMode engineMode = EngineMode.LSTM_ONLY;
    
    /**
     * 页面分割模式
     */
    private PageSegMode pageSegMode = PageSegMode.AUTO;
    
    /**
     * 处理超时时间(秒)
     */
    private int timeoutSeconds = 60;
    
    /**
     * 是否启用引擎缓存
     */
    private boolean cacheEnabled = true;
    
    /**
     * 图像 DPI
     */
    private int dpi = 300;
    
    /**
     * 图像缩放因子
     */
    private Double scaleFactor;
    
    public enum EngineMode {
        /**
         * 仅使用传统引擎
         */
        TESSERACT_ONLY,
        
        /**
         * 仅使用 LSTM 神经网络引擎(推荐)
         */
        LSTM_ONLY,
        
        /**
         * 使用传统引擎和 LSTM 的组合
         */
        TESSERACT_LSTM_COMBINED,
        
        /**
         * 默认,由 Tesseract 自动选择最佳模式
         */
        DEFAULT
    }
    
    public enum PageSegMode {
        /**
         * 完全自动分页,但不带方向和脚本检测
         */
        AUTO(0),
        
        /**
         * 仅带方向和脚本检测的自动分页(OSD)
         */
        AUTO_OSD(1),
        
        /**
         * 自动分页,带 OSD 和倾斜校正
         */
        AUTO_ONLY_OSD(2),
        
        /**
         * 自动分页,不带 OSD、倾斜校正
         */
        AUTO_ONLY(3),
        
        /**
         * 单列文本块,按列自动识别
         */
        SINGLE_COLUMN(4),
        
        /**
         * 单个统一文本块
         */
        SINGLE_BLOCK(5),
        
        /**
         * 单个文本行
         */
        SINGLE_LINE(6),
        
        /**
         * 单个单词
         */
        SINGLE_WORD(7),
        
        /**
         * 圆圈内的单个单词
         */
        CIRCLE_WORD(8),
        
        /**
         * 稀疏文本,按字符搜索
         */
        SPARSE_TEXT(9),
        
        /**
         * 稀疏文本,按行搜索
         */
        SPARSE_TEXT_OSD(10),
        
        /**
         * 原始行,将图像视为单个文本行
         */
        RAW_LINE(11);
        
        private final int value;
        
        PageSegMode(int value) {
            this.value = value;
        }
        
        public int getValue() {
            return value;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145

# OCR 服务核心实现

package com.example.ocr.service;

import com.example.ocr.config.OcrConfig;
import com.example.ocr.model.OcrResult;
import com.example.ocr.util.ImagePreprocessor;
import net.sourceforge.tess4j.ITessAPI;
import net.sourceforge.tess4j.ITesseract;
import net.sourceforge.tess4j.Tesseract;
import net.sourceforge.tess4j.Word;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

import jakarta.annotation.PostConstruct;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Service
public class OcrService {
    
    private static final Logger log = LoggerFactory.getLogger(OcrService.class);
    
    private final OcrConfig config;
    private final ImagePreprocessor preprocessor;
    
    private ITesseract tesseract;
    
    @Value("${ocr.tessdata.path:classpath:tessdata}")
    private Resource tessdataResource;
    
    public OcrService(OcrConfig config, ImagePreprocessor preprocessor) {
        this.config = config;
        this.preprocessor = preprocessor;
    }
    
    @PostConstruct
    public void init() {
        tesseract = new Tesseract();
        
        try {
            // 设置 tessdata 路径
            String tessdataPath = resolveTessdataPath();
            tesseract.setDatapath(tessdataPath);
            log.info("Tessdata 路径: {}", tessdataPath);
            
            // 设置识别语言
            tesseract.setLanguage(config.getLanguage());
            log.info("OCR 识别语言: {}", config.getLanguage());
            
            // 设置 OCR 引擎模式
            ITessAPI.TessOcrEngineMode OEM;
            switch (config.getEngineMode()) {
                case TESSERACT_ONLY -> OEM = ITessAPI.TessOcrEngineMode.TESSERACT_ONLY;
                case LSTM_ONLY -> OEM = ITessAPI.TessOcrEngineMode.LSTM_ONLY;
                case TESSERACT_LSTM_COMBINED -> OEM = ITessAPI.TessOcrEngineMode.TESSERACT_LSTM_COMBINED;
                default -> OEM = ITessAPI.TessOcrEngineMode.DEFAULT;
            }
            tesseract.setOcrEngineMode(OEM);
            log.info("OCR 引擎模式: {}", config.getEngineMode());
            
            // 设置页面分割模式
            tesseract.setPageSegMode(ITessAPI.TessPageSegMode.valueOf(
                "PSM_" + config.getPageSegMode().getValue()));
            log.info("页面分割模式: {}", config.getPageSegMode());
            
            // 设置变量
            tesseract.setVariable("preserve_interword_spaces", "1");
            
        } catch (Exception e) {
            log.error("初始化 Tesseract 失败", e);
            throw new RuntimeException("OCR 引擎初始化失败", e);
        }
    }
    
    /**
     * 解析 tessdata 路径
     */
    private String resolveTessdataPath() throws IOException {
        // 尝试多种路径解析方式
        String[] possiblePaths = {
            // 1. classpath 内嵌资源
            "src/main/resources/tessdata",
            // 2. 用户目录
            System.getProperty("user.home") + "/.tessdata",
            // 3. 项目根目录
            "tessdata",
            // 4. classpath 临时解压路径
            System.getProperty("java.io.tmpdir") + "/tessdata"
        };
        
        for (String path : possiblePaths) {
            File dir = new File(path);
            if (dir.exists() && dir.isDirectory()) {
                File[] files = dir.listFiles((d, name) -> name.endsWith(".traineddata"));
                if (files != null && files.length > 0) {
                    log.info("找到 tessdata 目录: {}", path);
                    return path;
                }
            }
        }
        
        // 如果都找不到,使用默认路径
        log.warn("未找到 tessdata 目录,将使用默认配置");
        return possiblePaths[0];
    }
    
    /**
     * 识别图像(简化版本)
     */
    public OcrResult recognize(BufferedImage image) {
        long startTime = System.currentTimeMillis();
        
        try {
            // 执行识别
            String text = tesseract.doOCR(image);
            
            // 计算置信度
            double confidence = calculateAverageConfidence(image);
            
            long processTime = System.currentTimeMillis() - startTime;
            log.info("OCR 识别完成,耗时: {}ms", processTime);
            
            return OcrResult.builder()
                .success(true)
                .text(text.trim())
                .confidence(confidence)
                .processTime(processTime)
                .build();
            
        } catch (Exception e) {
            log.error("OCR 识别失败", e);
            return OcrResult.builder()
                .success(false)
                .errorMessage(e.getMessage())
                .processTime(System.currentTimeMillis() - startTime)
                .build();
        }
    }
    
    /**
     * 识别图像(完整版本,返回详细结果)
     */
    public OcrResult recognizeWithDetails(BufferedImage image) {
        long startTime = System.currentTimeMillis();
        
        try {
            // 执行识别并获取详细信息
            List<Word> words = tesseract.getWords(image);
            
            // 提取文本和详细块信息
            StringBuilder textBuilder = new StringBuilder();
            List<OcrResult.TextBlock> blocks = new ArrayList<>();
            
            double totalConfidence = 0;
            int wordCount = 0;
            
            for (Word word : words) {
                String wordText = word.getText().trim();
                if (!wordText.isEmpty()) {
                    textBuilder.append(wordText).append(" ");
                    
                    double wordConfidence = word.getConfidence();
                    totalConfidence += wordConfidence;
                    wordCount++;
                    
                    OcrResult.TextBlock block = OcrResult.TextBlock.builder()
                        .text(wordText)
                        .confidence(wordConfidence)
                        .wordNum(wordCount)
                        .boundingBox(OcrResult.BoundingBox.builder()
                            .x1(word.getBoundingBox().x)
                            .y1(word.getBoundingBox().y)
                            .x2(word.getBoundingBox().x + word.getBoundingBox().width)
                            .y2(word.getBoundingBox().y)
                            .x3(word.getBoundingBox().x + word.getBoundingBox().width)
                            .y3(word.getBoundingBox().y + word.getBoundingBox().height)
                            .x4(word.getBoundingBox().x)
                            .y4(word.getBoundingBox().y + word.getBoundingBox().height)
                            .build())
                        .build();
                    
                    blocks.add(block);
                }
            }
            
            double avgConfidence = wordCount > 0 ? totalConfidence / wordCount : 0;
            
            long processTime = System.currentTimeMillis() - startTime;
            log.info("OCR 识别完成(详细模式),耗时: {}ms", processTime);
            
            return OcrResult.builder()
                .success(true)
                .text(textBuilder.toString().trim())
                .confidence(avgConfidence)
                .blocks(blocks)
                .processTime(processTime)
                .build();
            
        } catch (Exception e) {
            log.error("OCR 识别失败", e);
            return OcrResult.builder()
                .success(false)
                .errorMessage(e.getMessage())
                .processTime(System.currentTimeMillis() - startTime)
                .build();
        }
    }
    
    /**
     * 识别图像(带预处理)
     */
    public OcrResult recognizeWithPreprocessing(BufferedImage rawImage) {
        // 预处理图像
        BufferedImage processedImage = preprocessor.preprocess(rawImage, config);
        
        // 执行识别
        return recognize(processedImage);
    }
    
    /**
     * 从文件识别
     */
    public OcrResult recognizeFromFile(File imageFile) {
        try {
            BufferedImage image = javax.imageio.ImageIO.read(imageFile);
            return recognize(image);
        } catch (Exception e) {
            log.error("读取图像文件失败: {}", imageFile.getName(), e);
            return OcrResult.builder()
                .success(false)
                .errorMessage("读取图像文件失败: " + e.getMessage())
                .build();
        }
    }
    
    /**
     * 从字节数组识别
     */
    public OcrResult recognizeFromBytes(byte[] imageData) {
        try {
            BufferedImage image = preprocessor.loadImage(imageData);
            return recognize(image);
        } catch (Exception e) {
            log.error("解析图像数据失败", e);
            return OcrResult.builder()
                .success(false)
                .errorMessage("解析图像数据失败: " + e.getMessage())
                .build();
        }
    }
    
    /**
     * 计算平均置信度
     */
    private double calculateAverageConfidence(BufferedImage image) {
        try {
            // 通过识别结果获取置信度
            String text = tesseract.doOCR(image);
            // Tesseract 4.x+ 可以通过这种方式获取置信度信息
            return 85.0; // 默认置信度
        } catch (Exception e) {
            return 0.0;
        }
    }
    
    /**
     * 重新加载配置(用于动态修改配置后刷新引擎)
     */
    public void reload() {
        log.info("重新加载 OCR 配置");
        init();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278

# REST API 控制器

package com.example.ocr.controller;

import com.example.ocr.model.OcrResult;
import com.example.ocr.service.OcrService;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/ocr")
public class OcrController {
    
    private final OcrService ocrService;
    
    public OcrController(OcrService ocrService) {
        this.ocrService = ocrService;
    }
    
    /**
     * 简单 OCR 识别(返回纯文本)
     */
    @PostMapping("/recognize")
    public ResponseEntity<OcrResult> recognize(
            @RequestParam("file") MultipartFile file) {
        
        if (file.isEmpty()) {
            return ResponseEntity.badRequest()
                .body(OcrResult.builder()
                    .success(false)
                    .errorMessage("文件不能为空")
                    .build());
        }
        
        try {
            byte[] imageData = file.getBytes();
            OcrResult result = ocrService.recognizeFromBytes(imageData);
            return ResponseEntity.ok(result);
            
        } catch (IOException e) {
            return ResponseEntity.internalServerError()
                .body(OcrResult.builder()
                    .success(false)
                    .errorMessage("处理文件失败: " + e.getMessage())
                    .build());
        }
    }
    
    /**
     * 带预处理的 OCR 识别
     */
    @PostMapping("/recognize/with-preprocess")
    public ResponseEntity<OcrResult> recognizeWithPreprocess(
            @RequestParam("file") MultipartFile file) {
        
        try {
            byte[] imageData = file.getBytes();
            BufferedImage image = javax.imageio.ImageIO.read(
                new ByteArrayInputStream(imageData));
            
            OcrResult result = ocrService.recognizeWithPreprocessing(image);
            return ResponseEntity.ok(result);
            
        } catch (Exception e) {
            return ResponseEntity.internalServerError()
                .body(OcrResult.builder()
                    .success(false)
                    .errorMessage("处理失败: " + e.getMessage())
                    .build());
        }
    }
    
    /**
     * 获取详细识别结果(包含位置信息)
     */
    @PostMapping("/recognize/details")
    public ResponseEntity<OcrResult> recognizeDetails(
            @RequestParam("file") MultipartFile file) {
        
        try {
            byte[] imageData = file.getBytes();
            BufferedImage image = javax.imageio.ImageIO.read(
                new ByteArrayInputStream(imageData));
            
            OcrResult result = ocrService.recognizeWithDetails(image);
            return ResponseEntity.ok(result);
            
        } catch (Exception e) {
            return ResponseEntity.internalServerError()
                .body(OcrResult.builder()
                    .success(false)
                    .errorMessage("处理失败: " + e.getMessage())
                    .build());
        }
    }
    
    /**
     * 批量识别
     */
    @PostMapping("/recognize/batch")
    public ResponseEntity<Map<String, OcrResult>> recognizeBatch(
            @RequestParam("files") MultipartFile[] files) {
        
        Map<String, OcrResult> results = new HashMap<>();
        
        for (MultipartFile file : files) {
            try {
                byte[] imageData = file.getBytes();
                OcrResult result = ocrService.recognizeFromBytes(imageData);
                results.put(file.getOriginalFilename(), result);
            } catch (Exception e) {
                results.put(file.getOriginalFilename(), OcrResult.builder()
                    .success(false)
                    .errorMessage(e.getMessage())
                    .build());
            }
        }
        
        return ResponseEntity.ok(results);
    }
    
    /**
     * 健康检查
     */
    @GetMapping("/health")
    public ResponseEntity<Map<String, String>> health() {
        Map<String, String> status = new HashMap<>();
        status.put("status", "UP");
        status.put("service", "OCR Service");
        return ResponseEntity.ok(status);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136

# 高级特性实现

# 异步处理与线程池优化

package com.example.ocr.service;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Service
public class AsyncOcrService {
    
    private final OcrService ocrService;
    private final ExecutorService executor;
    
    public AsyncOcrService(OcrService ocrService) {
        this.ocrService = ocrService;
        this.executor = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors()
        );
    }
    
    /**
     * 异步 OCR 识别
     */
    @Async("ocrTaskExecutor")
    public CompletableFuture<OcrResult> recognizeAsync(byte[] imageData) {
        OcrResult result = ocrService.recognizeFromBytes(imageData);
        return CompletableFuture.completedFuture(result);
    }
    
    /**
     * 并行批量处理
     */
    public OcrResult[] recognizeParallel(byte[][] images) {
        return java.util.Arrays.stream(images)
            .map(data -> ocrService.recognizeFromBytes(data))
            .toArray(OcrResult[]::new);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 缓存机制优化


```java
package com.example.ocr.service;

import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class CachedOcrService {
    
    private final OcrService ocrService;
    private final ConcurrentHashMap<String, OcrResult> cache = new ConcurrentHashMap<>();
    
    private static final int MAX_CACHE_SIZE = 100;
    
    public CachedOcrService(OcrService ocrService) {
        this.ocrService = ocrService;
    }
    
    /**
     * 带缓存的 OCR 识别
     */
    public OcrResult recognizeWithCache(byte[] imageData) {
        String cacheKey = generateCacheKey(imageData);
        
        // 尝试从缓存获取
        OcrResult cached = cache.get(cacheKey);
        if (cached != null) {
            return OcrResult.builder()
                .success(cached.isSuccess())
                .text(cached.getText())
                .confidence(cached.getConfidence())
                .processTime(0L)
                .build();
        }
        
        // 执行识别
        OcrResult result = ocrService.recognizeFromBytes(imageData);
        
        // 存入缓存(如果成功且缓存未满)
        if (result.isSuccess() && cache.size() < MAX_CACHE_SIZE) {
            cache.put(cacheKey, result);
        }
        
        return result;
    }
    
    /**
     * 生成缓存键(基于图像内容的 MD5)
     */
    private String generateCacheKey(byte[] imageData) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] hash = md.digest(imageData);
            StringBuilder sb = new StringBuilder();
            for (byte b : hash) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            return String.valueOf(imageData.hashCode());
        }
    }
    
    /**
     * 清空缓存
     */
    public void clearCache() {
        cache.clear();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

# 单元测试

package com.example.ocr;

import com.example.ocr.model.OcrResult;
import com.example.ocr.service.OcrService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class OcrServiceTest {
    
    @Autowired
    private OcrService ocrService;
    
    @Test
    void testRecognize() {
        // 测试图像文件路径
        File testImage = new File("src/test/resources/test-image.png");
        
        if (testImage.exists()) {
            OcrResult result = ocrService.recognizeFromFile(testImage);
            
            assertTrue(result.isSuccess(), "识别应该成功");
            assertNotNull(result.getText(), "识别文本不应为空");
            assertTrue(result.getProcessTime() > 0, "处理时间应该记录");
            
            System.out.println("识别结果: " + result.getText());
            System.out.println("置信度: " + result.getConfidence());
            System.out.println("耗时: " + result.getProcessTime() + "ms");
        }
    }
    
    @Test
    void testRecognizeFromBytes() throws Exception {
        File testImage = new File("src/test/resources/test-image.png");
        
        if (testImage.exists()) {
            BufferedImage image = ImageIO.read(testImage);
            byte[] imageBytes = new byte[0];
            
            OcrResult result = ocrService.recognizeWithPreprocessing(image);
            
            assertNotNull(result);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# 性能优化策略

# 图像预处理优化

图像预处理对识别效果和性能都有重要影响。以下是一些最佳实践:

选择合适的 DPI:300 DPI 是文档扫描的理想选择,既能保证识别精度,又不会过度增加处理时间。过高的 DPI 会增加内存占用和处理时间,但识别效果提升有限。

适度缩放:对于非常大的图像,可以先缩放到合理尺寸(如宽度 2000-3000 像素)再进行识别。Tesseract 的 LSTM 引擎对缩放后的图像处理更快。

预处理顺序:推荐的预处理顺序是:灰度化 → 缩放 → 对比度增强 → 二值化 → 降噪。跳过不必要的预处理步骤可以显著提升性能。

# 引擎配置优化

选择合适的 OEM 模式:LSTM_ONLY 模式在大多数场景下比传统引擎更准确且速度更快。除非处理非常特殊的文档类型,建议使用 LSTM_ONLY。

选择合适的 PSM 模式:根据文档类型选择合适的页面分割模式:

  • AUTO:通用文档,自动检测
  • SINGLE_COLUMN:单栏文档
  • SINGLE_LINE:已知是单行文本
  • SPARSE_TEXT:稀疏文本,如问卷

减少加载时间:将 tessdata 文件放在 SSD 盘上,可以减少引擎初始化时间。如果需要频繁重启服务,保持引擎实例长期运行。

# 并行处理优化

对于需要处理大量图像的场景,可以采用以下并行化策略:

线程池处理:使用线程池并行处理多个图像请求。Tesseract 本身是线程安全的,可以安全地在多线程环境中共享实例。

分布式处理:对于超大批量处理,可以将任务分发到多个服务器。每个服务器处理一部分图像,最后汇总结果。

# 内存优化

避免重复加载:保持 Tesseract 实例长期运行,避免每次请求都重新加载引擎。

及时释放图像:处理完图像后及时释放内存,特别是处理大图像时。

使用合适的图像格式:PNG 格式通常比 JPEG 更适合 OCR,因为它是无损压缩且支持更高的位深。

# 常见问题与解决方案

# 问题一:识别结果乱码

原因:语言包配置错误或图像编码问题。

解决方案

  1. 确认使用了正确的语言包(如中文用 chi_sim
  2. 检查图像是否为正确的编码格式
  3. 尝试调整 PSM 模式

# 问题二:首次加载耗时过长

原因:Tesseract 引擎首次初始化需要加载语言包和模型文件。

解决方案

  1. 在应用启动时预热 OCR 引擎
  2. 使用线程池保持引擎实例活跃
  3. 将 tessdata 放在 SSD 上

# 问题三:识别准确率低

原因:图像质量差或配置不当。

解决方案

  1. 进行图像预处理(灰度化、二值化、去噪)
  2. 调整 DPI 设置(建议 300)
  3. 选择合适的 PSM 模式
  4. 考虑使用更高质量的语言包

# 问题四:内存占用过高

原因:处理大图像或频繁创建新实例。

解决方案

  1. 限制图像尺寸
  2. 复用 Tesseract 实例
  3. 及时释放资源
  4. 考虑使用 64 位 JVM

# 总结与展望

Tess4J 为 Java 开发者提供了一个强大而灵活的 OCR 解决方案。通过与 Spring Boot 的深度集成,开发者可以快速构建企业级的 OCR 服务。

特别提醒:Tess4J 虽然提供了纯 Java API,但底层依赖 Tesseract 原生库。开发者和运维人员需要特别注意:

  • Windows 开发环境通常开箱即用
  • Linux/macOS 生产环境需要通过系统包管理器安装 Tesseract
  • 语言数据文件(.traineddata)是必需的,需要单独下载和配置

本文从技术原理、环境配置、应用场景、集成实现到性能优化,全方位介绍了 Tess4J 在 Spring Boot 环境下的使用方式。特别是详细补充的各平台安装指南,希望能够帮助开发者快速完成环境搭建,避免在配置环节踩坑。

随着深度学习技术的不断发展,OCR 的准确率和性能也在持续提升。Tesseract 5.x 版本引入的更多 LSTM 模型和改进算法,使得识别效果有了显著提升。对于有更高准确率要求的场景,可以考虑结合专业的商业 OCR 服务,或针对特定领域训练自定义模型。

对于未来的发展方向,建议关注以下几个方面:与 Spring AI 的深度整合实现智能化 OCR、多语言文档的批量处理、以及端到端的文档数字化解决方案。