
在数字化浪潮席卷全球的今天,我们每天都在与海量的图片、文档打交道。从身份证、发票到广告牌、手写笔记,这些视觉信息中蕴含着宝贵的文字数据。然而,手动输入这些文字不仅效率低下,还容易出错。想象一下,如果你收到一份包含 100 页表格的扫描件,需要将其中的数据录入系统,这将是一项多么繁琐的工作!
光学字符识别(Optical Character Recognition,简称 OCR)技术正是解决这一痛点的关键。它能够自动识别图像中的文字并将其转换为可编辑的文本,极大地提高了信息处理效率。根据市场研究机构 Grand View Research 的数据,全球 OCR 市场规模预计将从 2023 年的 105 亿美元增长到 2030 年的 304 亿美元,年复合增长率高达 16.2%,足见其重要性和发展潜力。
本文将手把手教你如何在 Spring Boot 应用中集成 OCR 功能,从基础概念到实战代码,从简单识别到高级优化,让你的应用快速拥有 "读懂" 图片的能力。无论你是需要处理扫描文档、识别验证码,还是开发图文识别应用,都能从本文中找到实用的解决方案。
光学字符识别(OCR)是指通过电子设备(如扫描仪、摄像头)将纸质文档或图片中的文字转换为可编辑的数字文本的过程。简单来说,OCR 就是让计算机 "看懂" 图片中的文字。
OCR 技术的核心目标是:
OCR 的工作流程可以分为以下几个主要步骤:

目前主流的 OCR 技术和工具可以分为以下几类:
各类 OCR 技术的对比:
特性 | 开源 OCR 引擎 | 商业 OCR 服务 | 本地商业 OCR SDK |
|---|---|---|---|
成本 | 免费 | 按调用次数收费 | 一次性授权费用 |
准确率 | 中等 | 高 | 极高 |
部署方式 | 本地部署 | 云端调用 | 本地部署 |
灵活性 | 高,可定制 | 低,按服务提供 | 中,部分可定制 |
多语言支持 | 一般 | 好 | 优秀 |
离线支持 | 支持 | 不支持 | 支持 |
开发难度 | 中高 | 低 | 中 |
选择哪种 OCR 技术,需要根据项目的实际需求,综合考虑成本、准确率、部署方式、语言支持等因素。对于大多数中小规模应用,开源的 Tesseract OCR 或云 OCR 服务是不错的选择。
OCR 技术的应用非常广泛,涵盖了各行各业:
随着深度学习技术的发展,OCR 的识别准确率和适用场景还在不断提升,为更多行业带来效率提升和创新可能。
在 Spring Boot 应用中集成 OCR 功能,主要有两种方案:集成开源 OCR 引擎(如 Tesseract)和调用云 OCR 服务 API。下面我们分别介绍这两种方案的实现方式。
Tesseract OCR 是由 Google 开发并开源的 OCR 引擎,最初由 HP 公司于 1985 年开发,2005 年由 Google 接手维护。它支持多种操作系统和编程语言,识别准确率高,尤其是在经过训练后,对特定场景的识别效果非常好。
优点:
缺点:
各大云服务商(如百度、阿里、谷歌、亚马逊等)都提供了 OCR 服务,通过 API 接口即可调用。这些服务通常基于深度学习模型,识别准确率高,且针对不同场景做了优化。
优点:
缺点:
选择哪种方案,需要根据项目的实际需求:
接下来,我们将详细介绍这两种方案在 Spring Boot 中的具体实现。
Windows 系统:
tesseract --version,如果显示版本信息,则安装成功macOS 系统: 使用 Homebrew 安装:
brew install tesseract
brew install tesseract-lang # 安装多语言包
Linux 系统(Ubuntu/Debian):
sudoapt-getinstall tesseract-ocr
sudoapt-getinstall tesseract-ocr-chi-sim # 安装中文语言包
Tesseract 默认可能不包含中文等语言的训练数据,需要单独安装:
使用 Spring Initializr 创建一个新的 Spring Boot 项目,选择以下依赖:
<?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.2.0</version>
<relativePath/>
</parent>
<groupId>com.jam.ocr</groupId>
<artifactId>spring-boot-tesseract-ocr</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-tesseract-ocr</name>
<description>Spring Boot集成Tesseract OCR示例</description>
<properties>
<java.version>17</java.version>
<tess4j.version>5.8.0</tess4j.version>
<commons-lang3.version>3.14.0</commons-lang3.version>
<springdoc.version>2.3.0</springdoc.version>
<thumbnailator.version>0.4.20</thumbnailator.version>
</properties>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- Tesseract OCR Java绑定 -->
<dependency>
<groupId>net.sourceforge.tess4j</groupId>
<artifactId>tess4j</artifactId>
<version>${tess4j.version}</version>
</dependency>
<!-- 图像处理工具 -->
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>${thumbnailator.version}</version>
</dependency>
<!-- Commons Lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<!-- SpringDoc OpenAPI (Swagger 3) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>其中,tess4j 是 Tesseract OCR 的 Java 封装库,方便在 Java 应用中调用 Tesseract 功能。
server:
port: 8080
servlet:
context-path: /ocr
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
# OCR配置
ocr:
tesseract:
# Tesseract安装目录下的tessdata文件夹路径
# Windows系统示例: C:/Program Files/Tesseract-OCR/tessdata
# Linux/macOS系统示例: /usr/share/tesseract-ocr/4.00/tessdata
data-path: /usr/share/tesseract-ocr/4.00/tessdata
# 默认语言
default-language: chi_sim+eng
# 日志配置
logging:
level:
com.jam.ocr: INFO
net.sourceforge.tess4j: WARN
# Swagger配置
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operationsSorter: method注意:tessdata 路径需要根据实际安装位置进行修改,否则会导致 Tesseract 无法找到语言包。
OCR 识别请求参数(OcrRequest.java)
package com.jam.ocr.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
/**
* OCR识别请求参数
*
* @author 果酱
*/
@Data
@Schema(description = "OCR识别请求参数")
public class OcrRequest {
/**
* 待识别的图片文件
*/
@Schema(description = "待识别的图片文件", required = true)
private MultipartFile imageFile;
/**
* 识别语言,默认值由配置文件指定
* 语言代码参考: https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html
* 例如: chi_sim(简体中文), eng(英文), chi_sim+eng(中英文混合)
*/
@Schema(description = "识别语言,例如: chi_sim(简体中文), eng(英文), chi_sim+eng(中英文混合)", example = "chi_sim+eng")
private String language;
/**
* 是否启用图像预处理
*/
@Schema(description = "是否启用图像预处理", example = "true")
private boolean preprocess = true;
}OCR 识别结果(OcrResult.java)
package com.jam.ocr.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* OCR识别结果
*
* @author 果酱
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "OCR识别结果")
public class OcrResult {
/**
* 识别状态:success-成功,error-失败
*/
@Schema(description = "识别状态:success-成功,error-失败", example = "success")
private String status;
/**
* 识别出的文本内容
*/
@Schema(description = "识别出的文本内容")
private String text;
/**
* 识别耗时(毫秒)
*/
@Schema(description = "识别耗时(毫秒)", example = "123")
private long costTime;
/**
* 错误信息,当status为error时不为空
*/
@Schema(description = "错误信息,当status为error时不为空")
private String errorMsg;
/**
* 识别时间
*/
@Schema(description = "识别时间", example = "2023-11-01T12:34:56")
private LocalDateTime recognizeTime;
}
图像预处理对 OCR 识别准确率影响很大,通过灰度化、二值化、降噪等处理,可以显著提高识别效果。
package com.jam.ocr.util;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.util.CollectionUtils;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* 图像处理工具类
* 提供图像预处理功能,提高OCR识别准确率
*
* @author 果酱
*/
@Slf4j
public class ImageProcessUtils {
/**
* 对图像进行预处理
* 包括:调整大小、灰度化、二值化、降噪
*
* @param inputStream 图像输入流
* @return 处理后的图像输入流
* @throws IOException 处理过程中发生IO异常
*/
public static InputStream preprocessImage(InputStream inputStream) throws IOException {
log.info("开始图像预处理");
// 读取图像
BufferedImage image = ImageIO.read(inputStream);
Objects.requireNonNull(image, "无法读取图像文件");
// 调整图像大小,限制最大尺寸为1000x1000,保持比例
BufferedImage resizedImage = resizeImage(image, 1000, 1000);
// 灰度化处理
BufferedImage grayImage = convertToGray(resizedImage);
// 二值化处理
BufferedImage binaryImage = binarizeImage(grayImage);
// 降噪处理
BufferedImage denoisedImage = removeNoise(binaryImage);
// 将处理后的图像转换为输入流
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(denoisedImage, "png", outputStream);
log.info("图像预处理完成");
return new ByteArrayInputStream(outputStream.toByteArray());
}
/**
* 调整图像大小
*
* @param image 原始图像
* @param maxWidth 最大宽度
* @param maxHeight 最大高度
* @return 调整后的图像
* @throws IOException 处理过程中发生IO异常
*/
private static BufferedImage resizeImage(BufferedImage image, int maxWidth, int maxHeight) throws IOException {
int originalWidth = image.getWidth();
int originalHeight = image.getHeight();
// 计算缩放比例
double widthRatio = (double) maxWidth / originalWidth;
double heightRatio = (double) maxHeight / originalHeight;
double scaleRatio = Math.min(widthRatio, heightRatio);
// 如果不需要缩放,则直接返回原图
if (scaleRatio >= 1.0) {
return image;
}
int newWidth = (int) (originalWidth * scaleRatio);
int newHeight = (int) (originalHeight * scaleRatio);
log.info("调整图像大小: {}x{} -> {}x{}", originalWidth, originalHeight, newWidth, newHeight);
// 使用Thumbnails库进行图像缩放
return Thumbnails.of(image)
.size(newWidth, newHeight)
.keepAspectRatio(true)
.asBufferedImage();
}
/**
* 将彩色图像转换为灰度图像
*
* @param image 原始图像
* @return 灰度图像
*/
private static BufferedImage convertToGray(BufferedImage image) {
int width = image.getWidth();
int height = image.getHeight();
BufferedImage grayImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
Graphics g = grayImage.getGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
return grayImage;
}
/**
* 对灰度图像进行二值化处理
* 使用大津法(OTSU)自动确定阈值
*
* @param grayImage 灰度图像
* @return 二值化图像
*/
private static BufferedImage binarizeImage(BufferedImage grayImage) {
int width = grayImage.getWidth();
int height = grayImage.getHeight();
// 获取图像像素数据
int[] pixels = new int[width * height];
grayImage.getRGB(0, 0, width, height, pixels, 0, width);
// 计算直方图
int[] histogram = new int[256];
for (int pixel : pixels) {
int gray = (pixel & 0xFF);
histogram[gray]++;
}
// 使用大津法计算最佳阈值
int threshold = otsuThreshold(histogram, width * height);
log.info("二值化阈值: {}", threshold);
// 应用阈值进行二值化
BufferedImage binaryImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int index = y * width + x;
int gray = (pixels[index] & 0xFF);
int color = (gray >= threshold) ? 0xFFFFFFFF : 0xFF000000;
binaryImage.setRGB(x, y, color);
}
}
return binaryImage;
}
/**
* 大津法(OTSU)计算最佳阈值
*
* @param histogram 灰度直方图
* @param totalPixels 总像素数
* @return 最佳阈值
*/
private static int otsuThreshold(int[] histogram, int totalPixels) {
int threshold = 0;
double maxVariance = 0.0;
double totalMean = 0.0;
// 计算总均值
for (int i = 0; i < 256; i++) {
totalMean += i * histogram[i];
}
totalMean /= totalPixels;
int backgroundPixels = 0;
double backgroundMean = 0.0;
// 遍历所有可能的阈值,寻找最大类间方差
for (int i = 0; i < 256; i++) {
backgroundPixels += histogram[i];
if (backgroundPixels == 0) {
continue;
}
int foregroundPixels = totalPixels - backgroundPixels;
if (foregroundPixels == 0) {
break;
}
backgroundMean += i * histogram[i];
double foregroundMean = (totalMean * totalPixels - backgroundMean) / foregroundPixels;
// 计算类间方差
double variance = (double) backgroundPixels * foregroundPixels * Math.pow(backgroundMean / backgroundPixels - foregroundMean, 2);
// 更新最大方差和阈值
if (variance > maxVariance) {
maxVariance = variance;
threshold = i;
}
}
return threshold;
}
/**
* 对二值化图像进行降噪处理
* 使用简单的中值滤波算法
*
* @param binaryImage 二值化图像
* @return 降噪后的图像
*/
private static BufferedImage removeNoise(BufferedImage binaryImage) {
int width = binaryImage.getWidth();
int height = binaryImage.getHeight();
BufferedImage denoisedImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY);
// 3x3中值滤波
int[] kernel = new int[9];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// 获取3x3邻域像素
int index = 0;
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
int nx = Math.max(0, Math.min(width - 1, x + dx));
int ny = Math.max(0, Math.min(height - 1, y + dy));
kernel[index++] = binaryImage.getRGB(nx, ny);
}
}
// 对邻域像素排序,取中值
java.util.Arrays.sort(kernel);
int median = kernel[4]; // 中值
denoisedImage.setRGB(x, y, median);
}
}
return denoisedImage;
}
}
OCR 服务接口(OcrService.java)
package com.jam.ocr.service;
import com.jam.ocr.entity.OcrResult;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
/**
* OCR服务接口
* 提供图片文字识别功能
*
* @author 果酱
*/
public interface OcrService {
/**
* 识别图片中的文字
*
* @param imageFile 图片文件
* @param language 识别语言
* @param preprocess 是否启用图像预处理
* @return 识别结果
* @throws IOException 处理图片时发生IO异常
*/
OcrResult recognizeText(MultipartFile imageFile, String language, boolean preprocess) throws IOException;
}OCR 服务实现(TesseractOcrServiceImpl.java)
package com.jam.ocr.service.impl;
import com.jam.ocr.entity.OcrResult;
import com.jam.ocr.service.OcrService;
import com.jam.ocr.util.ImageProcessUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.sourceforge.tess4j.ITesseract;
import net.sourceforge.tess4j.TesseractException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
/**
* Tesseract OCR服务实现类
*
* @author 果酱
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class TesseractOcrServiceImpl implements OcrService {
private final ITesseract tesseract;
/**
* 默认识别语言
*/
@Value("${ocr.tesseract.default-language}")
private String defaultLanguage;
@Override
public OcrResult recognizeText(MultipartFile imageFile, String language, boolean preprocess) throws IOException {
log.info("开始OCR识别,文件名: {}, 语言: {}, 是否预处理: {}",
imageFile.getOriginalFilename(), language, preprocess);
// 参数校验
if (imageFile.isEmpty()) {
log.error("图片文件为空");
return OcrResult.builder()
.status("error")
.errorMsg("图片文件不能为空")
.recognizeTime(LocalDateTime.now())
.build();
}
// 确定识别语言
String lang = StringUtils.hasText(language) ? language : defaultLanguage;
tesseract.setLanguage(lang);
long startTime = System.currentTimeMillis();
OcrResult result;
try (InputStream inputStream = imageFile.getInputStream()) {
// 图像预处理
InputStream processInputStream = preprocess ?
ImageProcessUtils.preprocessImage(inputStream) : inputStream;
// 执行OCR识别
String text = tesseract.doOCR(processInputStream);
// 计算耗时
long costTime = System.currentTimeMillis() - startTime;
log.info("OCR识别完成,耗时: {}ms,识别结果长度: {}字符", costTime, text.length());
// 构建成功结果
result = OcrResult.builder()
.status("success")
.text(text)
.costTime(costTime)
.recognizeTime(LocalDateTime.now())
.build();
} catch (TesseractException e) {
log.error("OCR识别失败", e);
long costTime = System.currentTimeMillis() - startTime;
result = OcrResult.builder()
.status("error")
.errorMsg("识别失败: " + e.getMessage())
.costTime(costTime)
.recognizeTime(LocalDateTime.now())
.build();
}
return result;
}
}
package com.jam.ocr.config;
import net.sourceforge.tess4j.ITesseract;
import net.sourceforge.tess4j.Tesseract;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Tesseract OCR配置类
*
* @author 果酱
*/
@Configuration
public class TesseractConfig {
/**
* Tesseract数据文件路径
*/
@Value("${ocr.tesseract.data-path}")
private String tessDataPath;
/**
* 创建Tesseract实例
*
* @return ITesseract实例
*/
@Bean
public ITesseract tesseract() {
ITesseract tesseract = new Tesseract();
// 设置tessdata目录路径
tesseract.setDatapath(tessDataPath);
return tesseract;
}
}
package com.jam.ocr.controller;
import com.jam.ocr.entity.OcrRequest;
import com.jam.ocr.entity.OcrResult;
import com.jam.ocr.service.OcrService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
/**
* OCR控制器
* 提供图片文字识别API接口
*
* @author 果酱
*/
@Slf4j
@RestController
@RequestMapping("/api/tesseract")
@RequiredArgsConstructor
@Tag(name = "Tesseract OCR接口", description = "提供基于Tesseract的图片文字识别功能")
public class TesseractOcrController {
private final OcrService ocrService;
/**
* 识别图片中的文字
*
* @param request OCR识别请求参数
* @return 识别结果
*/
@PostMapping("/recognize")
@Operation(
summary = "识别图片中的文字",
description = "上传图片文件,识别其中的文字内容,支持多种语言",
responses = {
@ApiResponse(responseCode = "200", description = "识别成功",
content = @Content(schema = @Schema(implementation = OcrResult.class))),
@ApiResponse(responseCode = "400", description = "请求参数错误")
}
)
public ResponseEntity<OcrResult> recognizeText(OcrRequest request) {
log.info("收到OCR识别请求,文件名: {}",
request.getImageFile() != null ? request.getImageFile().getOriginalFilename() : "空");
try {
OcrResult result = ocrService.recognizeText(
request.getImageFile(),
request.getLanguage(),
request.isPreprocess()
);
return ResponseEntity.ok(result);
} catch (IOException e) {
log.error("处理图片时发生错误", e);
OcrResult errorResult = OcrResult.builder()
.status("error")
.errorMsg("处理图片时发生错误: " + e.getMessage())
.recognizeTime(java.time.LocalDateTime.now())
.build();
return ResponseEntity.badRequest().body(errorResult);
}
}
}
package com.jam.ocr;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Spring Boot集成Tesseract OCR启动类
*
* @author 果酱
*/
@SpringBootApplication
@OpenAPIDefinition(
info = @Info(
title = "Spring Boot OCR图片文字识别API",
version = "1.0.0",
description = "基于Tesseract和云服务的图片文字识别接口"
)
)
public class SpringBootTesseractOcrApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootTesseractOcrApplication.class, args);
}
}
运行启动类,确保应用成功启动,Tesseract 配置正确,没有出现找不到 tessdata 或语言包的错误。
访问 Swagger UI:http://localhost:8080/ocr/swagger-ui.html
找到Tesseract OCR接口下的/api/tesseract/recognize接口,点击 "Try it out",上传一张包含文字的图片,点击 "Execute" 按钮进行测试。
如果识别效果不理想,可以尝试以下优化措施:
图像预处理优化
调整预处理参数,或添加更多预处理步骤(如倾斜校正)。
语言包选择
根据图片中的语言选择合适的语言包,混合语言需要用 "+" 分隔。
训练自定义模型
对于特定字体或场景,可以使用 Tesseract 的训练工具生成自定义模型。
调整识别参数
通过 tesseract.setTessVariable () 方法设置识别参数,如:
设置识别模式为单行文本:
tesseract.setTessVariable("tessedit_pageseg_mode","7");Tesseract 的页面分割模式(pageseg_mode)参数说明:
根据实际场景选择合适的模式,可以提高识别准确率。
除了本地部署的 Tesseract OCR,我们还可以集成云 OCR 服务。百度云 OCR 提供了丰富的 API,对中文识别支持良好,且提供了多种专项识别功能(如身份证、发票、车牌等)。下面我们介绍如何在 Spring Boot 中集成百度云 OCR。
百度云 OCR 提供了多种识别服务,主要包括:
详细信息可以参考百度云 OCR 官方文档。
在之前的项目中添加百度云 OCR SDK 依赖,修改 pom.xml:
<!-- 百度云OCR SDK -->
<dependency>
<groupId>com.baidu.aip</groupId>
<artifactId>java-sdk</artifactId>
<version>4.16.14</version>
</dependency>
在 application.yml 中添加百度云 OCR 配置:
# 百度云OCR配置
ocr:
baidu:
app-id: your_app_id
api-key: your_api_key
secret-key: your_secret_key
# 接口调用超时时间(毫秒)
connect-timeout: 30000
# 读取数据超时时间(毫秒)
socket-timeout: 60000
将your_app_id、your_api_key、your_secret_key替换为实际的值。
package com.jam.ocr.config;
import com.baidu.aip.ocr.AipOcr;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 百度云OCR配置类
*
* @author 果酱
*/
@Configuration
public class BaiduOcrConfig {
@Value("${ocr.baidu.app-id}")
private String appId;
@Value("${ocr.baidu.api-key}")
private String apiKey;
@Value("${ocr.baidu.secret-key}")
private String secretKey;
@Value("${ocr.baidu.connect-timeout}")
private int connectTimeout;
@Value("${ocr.baidu.socket-timeout}")
private int socketTimeout;
/**
* 创建AipOcr实例
* AipOcr是百度云OCR的Java客户端,所有接口调用都通过该实例进行
*
* @return AipOcr实例
*/
@Bean
public AipOcr aipOcr() {
// 初始化一个AipOcr
AipOcr client = new AipOcr(appId, apiKey, secretKey);
// 可选:设置网络连接参数
client.setConnectionTimeoutInMillis(connectTimeout);
client.setSocketTimeoutInMillis(socketTimeout);
// 可选:设置代理服务器地址, http和socket二选一,或者均不设置
// client.setHttpProxy("proxy_host", proxy_port); // 设置http代理
// client.setSocketProxy("proxy_host", proxy_port); // 设置socket代理
return client;
}
}
package com.jam.ocr.service.impl;
import com.baidu.aip.ocr.AipOcr;
import com.baidu.aip.ocr.model.GeneralBasicOcrRequest;
import com.baidu.aip.ocr.model.GeneralBasicOcrResponse;
import com.baidu.aip.ocr.model.GeneralEnhancedOcrRequest;
import com.baidu.aip.ocr.model.GeneralEnhancedOcrResponse;
import com.baidu.aip.util.Base64Util;
import com.jam.ocr.entity.OcrResult;
import com.jam.ocr.service.OcrService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONObject;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 百度云OCR服务实现类
*
* @author 果酱
*/
@Slf4j
@Service("baiduOcrService")
@RequiredArgsConstructor
public class BaiduOcrServiceImpl implements OcrService {
private final AipOcr aipOcr;
/**
* 通用文字识别(高精度版)
*/
public static final String TYPE_GENERAL_ENHANCED = "general_enhanced";
/**
* 通用文字识别(标准版)
*/
public static final String TYPE_GENERAL_BASIC = "general_basic";
/**
* 识别图片中的文字
* 调用百度云OCR的通用文字识别接口
*
* @param imageFile 图片文件
* @param language 识别语言,百度云OCR会自动识别语言,此参数可忽略
* @param preprocess 是否启用图像预处理,百度云OCR内部会处理,此参数可忽略
* @return 识别结果
* @throws IOException 处理图片时发生IO异常
*/
@Override
public OcrResult recognizeText(MultipartFile imageFile, String language, boolean preprocess) throws IOException {
return recognizeText(imageFile, TYPE_GENERAL_ENHANCED);
}
/**
* 识别图片中的文字,指定识别类型
*
* @param imageFile 图片文件
* @param ocrType 识别类型,如general_basic(标准版)、general_enhanced(高精度版)
* @return 识别结果
* @throws IOException 处理图片时发生IO异常
*/
public OcrResult recognizeText(MultipartFile imageFile, String ocrType) throws IOException {
log.info("开始百度云OCR识别,文件名: {}, 识别类型: {}",
imageFile.getOriginalFilename(), ocrType);
// 参数校验
if (imageFile.isEmpty()) {
log.error("图片文件为空");
return OcrResult.builder()
.status("error")
.errorMsg("图片文件不能为空")
.recognizeTime(LocalDateTime.now())
.build();
}
// 检查文件类型
String contentType = imageFile.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
log.error("文件不是图片类型,Content-Type: {}", contentType);
return OcrResult.builder()
.status("error")
.errorMsg("请上传图片文件")
.recognizeTime(LocalDateTime.now())
.build();
}
long startTime = System.currentTimeMillis();
OcrResult result;
try {
// 将图片转换为Base64编码
byte[] imageData = imageFile.getBytes();
String base64Image = Base64Util.encode(imageData);
// 设置请求参数
Map<String, String> options = new HashMap<>(4);
// 是否检测图像朝向
options.put("detect_direction", "true");
// 是否检测语言
options.put("detect_language", "true");
// 调用百度云OCR API
JSONObject response;
if (TYPE_GENERAL_ENHANCED.equals(ocrType)) {
// 通用文字识别(高精度版)
GeneralEnhancedOcrRequest request = new GeneralEnhancedOcrRequest(base64Image, options);
GeneralEnhancedOcrResponse res = aipOcr.generalEnhanced(request);
response = res.getJsonObject();
} else {
// 通用文字识别(标准版)
GeneralBasicOcrRequest request = new GeneralBasicOcrRequest(base64Image, options);
GeneralBasicOcrResponse res = aipOcr.generalBasic(request);
response = res.getJsonObject();
}
// 计算耗时
long costTime = System.currentTimeMillis() - startTime;
// 处理响应结果
if (response.has("error_code")) {
// 识别失败
String errorCode = response.getString("error_code");
String errorMsg = response.getString("error_msg");
log.error("百度云OCR识别失败,错误码: {}, 错误信息: {}", errorCode, errorMsg);
result = OcrResult.builder()
.status("error")
.errorMsg("识别失败: " + errorCode + " - " + errorMsg)
.costTime(costTime)
.recognizeTime(LocalDateTime.now())
.build();
} else {
// 识别成功,提取文字内容
List<JSONObject> wordsResult = response.getJSONArray("words_result").toList()
.stream()
.map(obj -> new JSONObject((Map<?, ?>) obj))
.collect(Collectors.toList());
StringBuilder textBuilder = new StringBuilder();
for (JSONObject word : wordsResult) {
textBuilder.append(word.getString("words")).append("\n");
}
String text = textBuilder.toString().trim();
log.info("百度云OCR识别完成,耗时: {}ms,识别结果长度: {}字符", costTime, text.length());
result = OcrResult.builder()
.status("success")
.text(text)
.costTime(costTime)
.recognizeTime(LocalDateTime.now())
.build();
}
} catch (Exception e) {
log.error("百度云OCR识别发生异常", e);
long costTime = System.currentTimeMillis() - startTime;
result = OcrResult.builder()
.status("error")
.errorMsg("识别发生异常: " + e.getMessage())
.costTime(costTime)
.recognizeTime(LocalDateTime.now())
.build();
}
return result;
}
/**
* 身份证识别
*
* @param imageFile 身份证图片文件
* @param idCardSide 身份证正反面,front-正面,back-反面
* @return 识别结果,包含姓名、身份证号等信息
* @throws IOException 处理图片时发生IO异常
*/
public OcrResult recognizeIdCard(MultipartFile imageFile, String idCardSide) throws IOException {
log.info("开始身份证识别,文件名: {}, 正反面: {}", imageFile.getOriginalFilename(), idCardSide);
// 参数校验
if (imageFile.isEmpty()) {
log.error("图片文件为空");
return OcrResult.builder()
.status("error")
.errorMsg("图片文件不能为空")
.recognizeTime(LocalDateTime.now())
.build();
}
if (StringUtils.isBlank(idCardSide) || (!"front".equals(idCardSide) && !"back".equals(idCardSide))) {
log.error("身份证正反面参数错误: {}", idCardSide);
return OcrResult.builder()
.status("error")
.errorMsg("身份证正反面参数错误,必须为front或back")
.recognizeTime(LocalDateTime.now())
.build();
}
long startTime = System.currentTimeMillis();
OcrResult result;
try {
// 将图片转换为Base64编码
byte[] imageData = imageFile.getBytes();
String base64Image = Base64Util.encode(imageData);
// 设置请求参数
Map<String, String> options = new HashMap<>(2);
// 是否开启身份证风险类型(身份证复印件、临时身份证、身份证翻拍)检测
options.put("detect_risk", "true");
// 调用身份证识别API
JSONObject response = aipOcr.idcard(base64Image, idCardSide, options);
// 计算耗时
long costTime = System.currentTimeMillis() - startTime;
// 处理响应结果
if (response.has("error_code")) {
// 识别失败
String errorCode = response.getString("error_code");
String errorMsg = response.getString("error_msg");
log.error("身份证识别失败,错误码: {}, 错误信息: {}", errorCode, errorMsg);
result = OcrResult.builder()
.status("error")
.errorMsg("识别失败: " + errorCode + " - " + errorMsg)
.costTime(costTime)
.recognizeTime(LocalDateTime.now())
.build();
} else {
// 识别成功,提取身份证信息
JSONObject idCardResult = response.getJSONObject("words_result");
StringBuilder textBuilder = new StringBuilder();
// 正面信息
if ("front".equals(idCardSide)) {
textBuilder.append("姓名: ").append(idCardResult.getJSONObject("姓名").getString("words")).append("\n");
textBuilder.append("性别: ").append(idCardResult.getJSONObject("性别").getString("words")).append("\n");
textBuilder.append("民族: ").append(idCardResult.getJSONObject("民族").getString("words")).append("\n");
textBuilder.append("出生: ").append(idCardResult.getJSONObject("出生").getString("words")).append("\n");
textBuilder.append("住址: ").append(idCardResult.getJSONObject("住址").getString("words")).append("\n");
textBuilder.append("公民身份号码: ").append(idCardResult.getJSONObject("公民身份号码").getString("words")).append("\n");
}
// 反面信息
else {
textBuilder.append("签发机关: ").append(idCardResult.getJSONObject("签发机关").getString("words")).append("\n");
textBuilder.append("有效期限: ").append(idCardResult.getJSONObject("有效期限").getString("words")).append("\n");
}
// 风险信息
if (response.has("risk_type")) {
String riskType = response.getString("risk_type");
String riskMsg;
switch (riskType) {
case "normal":
riskMsg = "正常身份证";
break;
case "copy":
riskMsg = "身份证复印件";
break;
case "temporary":
riskMsg = "临时身份证";
break;
case "screen":
riskMsg = "身份证翻拍";
break;
default:
riskMsg = "未知风险类型: " + riskType;
}
textBuilder.append("风险检测: ").append(riskMsg).append("\n");
}
String text = textBuilder.toString().trim();
log.info("身份证识别完成,耗时: {}ms", costTime);
result = OcrResult.builder()
.status("success")
.text(text)
.costTime(costTime)
.recognizeTime(LocalDateTime.now())
.build();
}
} catch (Exception e) {
log.error("身份证识别发生异常", e);
long costTime = System.currentTimeMillis() - startTime;
result = OcrResult.builder()
.status("error")
.errorMsg("识别发生异常: " + e.getMessage())
.costTime(costTime)
.recognizeTime(LocalDateTime.now())
.build();
}
return result;
}
}
package com.jam.ocr.controller;
import com.jam.ocr.entity.OcrRequest;
import com.jam.ocr.entity.OcrResult;
import com.jam.ocr.service.impl.BaiduOcrServiceImpl;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
/**
* 百度云OCR控制器
* 提供基于百度云OCR的图片文字识别API接口
*
* @author 果酱
*/
@Slf4j
@RestController
@RequestMapping("/api/baidu")
@RequiredArgsConstructor
@Tag(name = "百度云OCR接口", description = "提供基于百度云的图片文字识别功能,包括通用识别和身份证识别等")
public class BaiduOcrController {
private final BaiduOcrServiceImpl baiduOcrService;
/**
* 通用文字识别
*
* @param imageFile 图片文件
* @param ocrType 识别类型,general_basic(标准版)或general_enhanced(高精度版)
* @return 识别结果
*/
@PostMapping("/recognize")
@Operation(
summary = "通用文字识别",
description = "上传图片文件,识别其中的文字内容,支持多种语言",
responses = {
@ApiResponse(responseCode = "200", description = "识别成功",
content = @Content(schema = @Schema(implementation = OcrResult.class))),
@ApiResponse(responseCode = "400", description = "请求参数错误")
}
)
public ResponseEntity<OcrResult> recognizeText(
@Parameter(description = "待识别的图片文件", required = true)
@RequestParam("imageFile") MultipartFile imageFile,
@Parameter(description = "识别类型,general_basic(标准版)或general_enhanced(高精度版)",
example = "general_enhanced")
@RequestParam(value = "ocrType", defaultValue = BaiduOcrServiceImpl.TYPE_GENERAL_ENHANCED)
String ocrType) {
log.info("收到百度云OCR识别请求,文件名: {}, 识别类型: {}",
imageFile.getOriginalFilename(), ocrType);
try {
OcrResult result = baiduOcrService.recognizeText(imageFile, ocrType);
return ResponseEntity.ok(result);
} catch (IOException e) {
log.error("处理图片时发生错误", e);
OcrResult errorResult = OcrResult.builder()
.status("error")
.errorMsg("处理图片时发生错误: " + e.getMessage())
.recognizeTime(java.time.LocalDateTime.now())
.build();
return ResponseEntity.badRequest().body(errorResult);
}
}
/**
* 身份证识别
*
* @param imageFile 身份证图片文件
* @param idCardSide 身份证正反面,front-正面,back-反面
* @return 识别结果,包含姓名、身份证号等信息
*/
@PostMapping("/recognize/id-card")
@Operation(
summary = "身份证识别",
description = "上传身份证图片,识别其中的信息,支持正反面识别",
responses = {
@ApiResponse(responseCode = "200", description = "识别成功",
content = @Content(schema = @Schema(implementation = OcrResult.class))),
@ApiResponse(responseCode = "400", description = "请求参数错误")
}
)
public ResponseEntity<OcrResult> recognizeIdCard(
@Parameter(description = "身份证图片文件", required = true)
@RequestParam("imageFile") MultipartFile imageFile,
@Parameter(description = "身份证正反面,front-正面,back-反面",
example = "front")
@RequestParam("idCardSide") String idCardSide) {
log.info("收到身份证识别请求,文件名: {}, 正反面: {}",
imageFile.getOriginalFilename(), idCardSide);
try {
OcrResult result = baiduOcrService.recognizeIdCard(imageFile, idCardSide);
return ResponseEntity.ok(result);
} catch (IOException e) {
log.error("处理图片时发生错误", e);
OcrResult errorResult = OcrResult.builder()
.status("error")
.errorMsg("处理图片时发生错误: " + e.getMessage())
.recognizeTime(java.time.LocalDateTime.now())
.build();
return ResponseEntity.badRequest().body(errorResult);
}
}
}
启动应用后,访问 Swagger UI:http://localhost:8080/ocr/swagger-ui.html
测试百度云 OCR 接口:
百度云OCR接口下的/api/baidu/recognize接口,上传图片进行测试。/api/baidu/recognize/id-card接口,上传身份证图片(正面或反面),指定idCardSide参数进行测试。百度云 OCR 提供的 API 有调用次数限制,免费额度用完后需要付费。可以在百度云控制台查看使用情况和计费标准。
无论是使用 Tesseract 还是云 OCR 服务,都可以通过一些优化策略提高识别效果和系统性能。
图像质量是影响 OCR 识别率的关键因素,良好的预处理可以显著提高识别准确率:
识别后的文本往往存在一些错误,可以通过后处理提高准确率:
对于需要处理大量图片的应用,性能优化至关重要:
针对不同的应用场景,可以采取特定的优化策略:
应用场景:企业员工报销时,上传发票图片,系统自动识别发票信息(发票号码、金额、日期等),自动填充报销单。
技术方案:
架构图:

关键代码片段:
/**
* 发票识别服务
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class InvoiceRecognitionService {
private final AipOcr aipOcr;
private final RestTemplate restTemplate;
@Value("${invoice.verify.api-url}")
private String verifyApiUrl;
@Value("${invoice.verify.app-key}")
private String verifyAppKey;
/**
* 识别发票信息并验真
*/
public InvoiceResult recognizeAndVerify(MultipartFile imageFile) throws IOException {
// 1. 调用百度云发票识别API
byte[] imageData = imageFile.getBytes();
String base64Image = Base64Util.encode(imageData);
Map<String, String> options = new HashMap<>();
options.put("type", "normal"); // 普通发票
JSONObject response = aipOcr.vatInvoice(base64Image, options);
// 2. 解析发票信息
InvoiceInfo invoiceInfo = parseInvoiceInfo(response);
// 3. 发票验真
boolean verified = verifyInvoice(invoiceInfo);
// 4. 返回结果
return InvoiceResult.builder()
.invoiceInfo(invoiceInfo)
.verified(verified)
.build();
}
/**
* 解析发票信息
*/
private InvoiceInfo parseInvoiceInfo(JSONObject response) {
// 解析响应,提取发票信息
// ...
}
/**
* 发票验真
*/
private boolean verifyInvoice(InvoiceInfo invoiceInfo) {
// 调用发票验真API
// ...
}
}
应用场景:图书馆或书店需要将图书封面信息(书名、作者、ISBN 等)录入系统,通过 OCR 识别自动提取。
技术方案:
关键优化点:
应用场景:银行、酒店等场所需要登记客户身份证信息,通过 OCR 识别自动录入,提高效率并减少错误。
技术方案:
安全考虑: