简介围绕 Java 实现图文 OCR 识别这一常见需求文档以方案对比为主线面向需要在中后台系统、票据或图片文字提取场景落地文字识别的开发者尤其适合具备一定 Java 基础、希望快速选型的中级工程师。内容梳理三条技术路线调用云端 OCR 接口的个人申请方案准确率高、响应快但存在每月请求次数限制与后续收费的不确定性基于 Tesseract 的开源方案免费但需先将图片写入 PDF 再逐页渲染识别准确率偏低且易出现乱码以及在前者基础上调整页面旋转角度、合并中英文语言库的优化拓展方案。文中配有 APPID、API KEY、SECRET KEY 初始化连接与 socket 超时、HTTP 代理、log4j 日志配置以及 PDFRenderer 按 300 DPI 渲染、chi_sim 语言设置等可直接参考的代码片段并逐条列出各方案优缺点便于按准确率、成本与请求量权衡取舍。压缩包仅含 1 个 docx约 12KB篇幅精炼、便于检索已有 273 人学习。1. 一张歪着拍的发票把 Java 端 OCR 的短板全暴露了把一套在扫描件上跑得不错的 JAVA-OCR 图文识别链路直接搬到手机拍照和传真件场景识别率往往会大幅下降。同一张报销单扫描件能把“壹万贰仟元整”完整读出来换成倾斜十五度、带阴影、短边不到 1200 像素的照片输出的却是被拆成三行的碎片金额字段直接错位。多数人第一反应是模型不行、语言包不够、要换更强的引擎但把图片拆开看问题几乎都出在输入端透视畸变没有纠正、光照不均导致全局二值化把浅色字抹平、分辨率不足以让 12 号字凑够笔画。这篇内容讲的就是这条链路用 Java 把图片和 PDF 归一化成可识别的位图做一套顺序正确的预处理再选择 Tess4J 本地引擎或 ONNX 中文检测识别模型做推理最后把识别结果落成能入库的字段。适合手上已经有一个 Java 服务、需要把票据、合同、证照、工单截图里的文字批量取出来的开发者也适合已经上了 OCR 但对准确率不满意、想搞清楚每个参数在干什么的人。全程只讲能复现的代码和参数不讲引擎内部论文。2. 三条路线怎么选Tess4J、ONNX 模型、商业接口的 Java 接入边界选型这一步比写代码更影响最终效果。JAVA-OCR 图文识别的可选路线其实只有三条而且它们的差别不在“准不准”在于你能不能接受它出错的方式。下面先把三条路线的适用范围讲清楚再给出两条本地路线的可运行代码。2.1 本地引擎、深度学习模型、商业接口各自适用的场景Tess4J 是对 Tesseract 的 JNA 封装本质是一个跑了十几年的传统 LSTM 混合引擎。它的优势是部署极轻一个 jar 加一个几十兆的语言包纯 CPU 跑无网络依赖数据不出机器许可证对商业项目友好。缺点是它对版面极其敏感双栏排版、无线框表格、文字压背景图这几种情况它倾向于按行硬拼输出顺序错乱中文印刷体在图片干净时能到 90% 上下一旦有噪点就掉得很快。深度学习路线是“文本检测 文本识别”两段式检测模型DBNet 一类先输出一堆文本框识别模型CRNN/SVTR 一类再逐框解码。它对密集小字、弯曲文本、复杂背景的鲁棒性明显好于传统引擎中文行文本是它的强项。代价是你得自己管模型文件、自己写前后处理模型转换和字典对齐是主要工作量。JVM 侧跑推理有两条常见路径一是用 ONNX Runtime 的 Java 绑定直接喂张量二是用 DJL 这类高层封装前者可控性更好后者上手快。商业 OCR 接口准确率最高尤其是手写体和特殊版面接入成本也最低——一个 HTTP 调用就完事。但它有三个硬约束按次计费导致批量场景成本不可控、数据要出你的机器、QPS 和超时由对方决定。常见做法是把它当兜底而不是主力本地引擎置信度低于阈值的那部分图片才转发出去。维度Tess4J 本地引擎ONNX 中文检测识别模型商业 OCR 接口单张耗时4 核 CPU量级参考0.3~1.5 秒0.2~0.8 秒0.3~1 秒另加网络往返干净印刷体中文中上高高手写体差中高复杂背景 / 弯曲文本差好好部署与运维成本低中极低数据是否出域否否是参数可控程度高高低我的取舍习惯是日处理量在几万张以内、图片以扫描件和截图为主、对数据出域敏感就走本地路线先用 Tess4J 打通链路再把识别层抽象成接口后面换 ONNX 模型时只替换实现类。如果手写票据占比超过三成本地路线的投入产出比就很差直接用接口兜底更省事。2.2 用 Tess4J 在本地跑通第一张中文图先确认三件事到位依赖引入、语言包落盘、本地库能加载。语言包只需要chi_sim.traineddata如果票据里有大量数字和英文编号再补一个eng.traineddata识别时用chi_simeng组合纯数字字段的准确率会好一截。import net.sourceforge.tess4j.Tesseract; import net.sourceforge.tess4j.ITessAPI; import net.sourceforge.tess4j.Word; import javax.imageio.ImageIO; import java.io.File; import java.util.List; public class OcrOnce { public static void main(String[] args) throws Exception { // tessdata 必须指向“目录”不是 .traineddata 文件本身 File dataDir new File(/opt/ocr/tessdata); Tesseract t new Tesseract(); t.setDatapath(dataDir.getAbsolutePath()); t.setLanguage(chi_simeng); // 中文为主英文数字兜底 t.setPageSegMode(6); // PSM 6假定整页是一整块文本 t.setOcrEngineMode(1); // OEM 1只用 LSTM速度更快 // 保留词间空格否则中文会被拆成单个字后处理更麻烦 t.setVariable(preserve_interword_spaces, 1); // 大图先降采样上限避免单页耗时失控 t.setVariable(user_defined_dpi, 300); var img ImageIO.read(new File(/opt/ocr/in/sample.png)); String text t.doOCR(img); System.out.println(text); // 逐词置信度用来做字段级质量分流 ListWord words t.getWords(img, ITessAPI.TessPageIteratorLevel.RIL_WORD); for (Word w : words) { if (w.getConfidence() 60) { System.out.println(低置信 w.getText() - w.getConfidence()); } } } }这段代码的关键不在doOCR那一行而在几个 setter 上。setDatapath接收的是包含语言包的目录传错成文件路径会抛“请设置 TESSDATA_PREFIX”的错这是新手最容易卡住的一步。setPageSegMode(6)把一个页面当成单一文本块处理适合票面、合同正文这类排版规整的图如果图片是零散的界面截图用 6 会把不相关的区域强行拼成一段这时候应该换 11稀疏文本。setOcrEngineMode(1)跳过传统引擎的融合打分只用 LSTM速度提升明显中文场景下准确率基本不降。getWords返回的置信度是 0 到 100 的整数直接用整页平均分做质量判断意义不大因为一页里总有几个字是低的。做法是按行聚合一行里超过三成词低于 60 就把整行标记为“待人工复核”这条标记比识别文本本身更有业务价值。2.3 把中文检测识别模型导出 ONNX在 JVM 里做前向模型路线的第一步是拿到两个 ONNX 文件检测模型输入1x3xHxW的归一化图像输出一张概率图和识别模型输入3x48x320的行图输出时间步 × 字符集维度的 logits。字符集字典要和训练时用的那份完全一致少一个字符后面整个索引全错位这类错位不会报异常只会输出一堆看起来很像字的乱码属于典型的黑匣子故障。import ai.onnxruntime.*; import java.nio.FloatBuffer; import java.util.Collections; public class DetRunner { public static void main(String[] args) throws Exception { OrtEnvironment env OrtEnvironment.getEnvironment(); OrtSession.SessionOptions opts new OrtSession.SessionOptions(); // 推理线程数和 Web 容器线程数错开别让两者抢同一批核 opts.setIntraOpNumThreads(Math.max(1, Runtime.getRuntime().availableProcessors() / 2)); opts.setOptimizationLevel(OrtSession.SessionOptions.OptLevel.ALL_OPT); try (OrtSession session env.createSession(/opt/ocr/model/det.onnx, opts)) { // NCHW1 x 3 x H x W像素需按 ImageNet 均值方差标准化 long[] shape new long[]{1, 3, 960, 960}; float[] data new float[1 * 3 * 960 * 960]; // 由预处理阶段填充 FloatBuffer buf FloatBuffer.wrap(data); OnnxTensor input OnnxTensor.createTensor(env, buf, shape); OrtSession.Result res session.run(Collections.singletonMap(x, input)); // 输出是概率图后续要做二值化 轮廓提取 unclip 才能得到文本框 float[][][][] prob (float[][][][]) res.get(0).getValue(); System.out.println(输出维度: prob.length x prob[0].length); } } }前向只占整条链路的两成工作量剩下八成在两个后处理上。检测这边概率图按box_thresh常见 0.5~0.6二值化后提轮廓再按unclip_ratio1.5~2.0做外扩因为检测模型给出的是文字核心区不外扩会把字的边缘切掉识别端就会读成半个字。识别这边输出要按 CTC 规则解码每个时间步取 argmax合并相邻重复去掉空白位。这两步的代码量不小好在逻辑是纯数组操作用 Java 写性能足够。给识别层定义一个接口把 Tess4J 和 ONNX 两条实现都挂上去业务层只依赖接口。这样切换引擎时上层的预处理、超时、置信度分流全都不用动。这个抽象在项目初期多花半天后期能省掉大量返工。3. Spring Boot 里的识别链路从 PDF、拍照图到结构化文本单机能跑通和能上线是两件事。上线要处理的是格式混杂、单页耗时不可控、部分图片根本识别不出来这几种情况。这一章按数据流向拆成三段入口归一化、预处理、执行层。3.1 输入归一化PDF、扫描件、手机照片统一成 BufferedImage入口第一个坑是用文件扩展名判断类型。用户上传的.jpg里装的可能是 PDF也可能是 HEIC扩展名一点都不可信。判断要靠魔数PDF 的文件头是25 50 44 46即%PDFJPEG 是FF D8 FFPNG 是89 50 4E 47。public BufferedImage normalize(byte[] bytes) throws IOException { String magic toHex(bytes, 4); if (magic.startsWith(25504446)) { // %PDF try (PDDocument doc PDDocument.load(bytes)) { PDFRenderer renderer new PDFRenderer(doc); // 300 DPI低分辨率扫描件常见的 150 DPI 会把小字号模糊到无法识别 return renderer.renderImageWithDPI(0, 300f, ImageType.RGB); } } if (magic.startsWith(FFD8FF) || magic.startsWith(89504E47)) { // JPEG / PNG BufferedImage raw ImageIO.read(new ByteArrayInputStream(bytes)); // 手机照片要按 EXIF Orientation 旋转否则整页横躺 return applyExifOrientation(raw, bytes); } if (magic.startsWith(49492A00) || magic.startsWith(4D4D002A)) { // TIFF可能多页 try (ImageInputStream iis ImageIO.createImageInputStream(new ByteArrayInputStream(bytes))) { IteratorImageReader it ImageIO.getImageReaders(iis); ImageReader reader it.next(); reader.setInput(iis); return reader.read(0); // 多页按页循环别只读第一页 } } throw new IllegalArgumentException(不支持的输入格式: magic); }PDF 渲染的 DPI 是这段里最该纠结的参数。150 DPI 渲染出来12 号字只剩十几个像素高笔画粘连300 DPI 是公认的起点A4 页面渲染出来约 2480×3508单张占内存 35MB 左右再往上到 600 DPI准确率提升已经很小耗时和内存却翻倍除非原件本身就是极小字号的合同扫描。EXIF 旋转这一步经常被跳过。手机拍照时传感器方向固定方向信息写在 EXIF 里ImageIO.read不会自动应用它结果就是整页文字旋转 90 度。Tesseract 对旋转 90 度的中文页几乎完全失效输出的是一堆无关字符而且不会报错。要么用元数据解析库读 Orientation 标签手动转要么交给前端在上传前用 canvas 矫正。多页 TIFF 也一样reader.read(0)只拿到第一页剩下的页会被静默丢弃业务方会以为系统漏识别了。3.2 预处理流水线灰度、纠偏、缩放、二值化的顺序不能乱预处理的顺序是有讲究的调换任意两步都会让效果变差。正确顺序是灰度 → 轻度去噪 → 纠偏 → 缩放 → 二值化。原因很直接纠偏需要在灰度图上算角度二值化之后的黑白边缘全是锯齿角度算出来会偏缩放在二值化之前做是双三次插值字边缘是平滑过渡二值化之后再缩放就是最近邻插值笔画直接断成点二值化放最后是因为前面的插值运算需要连续的灰度值。public static Mat preprocess(Mat src) { Mat gray new Mat(); if (src.channels() 3) { Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY); } else { src.copyTo(gray); } // 1) 纠偏先估角度只在小角度区间内转超范围说明不是歪而是版面复杂 double angle estimateSkew(gray); if (Math.abs(angle) 0.3 Math.abs(angle) 15) { Mat m Imgproc.getRotationMatrix2D( new Point(gray.cols() / 2.0, gray.rows() / 2.0), angle, 1.0); Imgproc.warpAffine(gray, gray, m, gray.size(), Imgproc.INTER_CUBIC, Core.BORDER_REPLICATE); } // 2) 缩放短边拉到 1600 左右小字识别率提升最明显的一步 double scale 1600.0 / Math.min(gray.cols(), gray.rows()); if (scale 1.05) { Imgproc.resize(gray, gray, new Size(), scale, scale, Imgproc.INTER_CUBIC); } // 3) 局部对比度增强对付光照不均和浅色字 Mat clahe new Mat(); Imgproc.createCLAHE(2.0, new Size(8, 8)).apply(gray, clahe); // 4) 自适应二值化blockSize 必须是奇数C 是偏移量 Mat bin new Mat(); Imgproc.adaptiveThreshold(clahe, bin, 255, Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C, Imgproc.THRESH_BINARY, 31, 15); // 5) 中值滤波去椒盐噪点核 3 足够再大就把细笔画抹掉了 Imgproc.medianBlur(bin, bin, 3); return bin; }estimateSkew的实现思路是对灰度图做 Canny 边缘检测用霍夫变换取直线统计所有直线的角度做中位数。只在 -15° 到 15° 之间生效是因为超过这个范围多半是版面本身有斜线装饰或表格框线强行旋转会把正常页面转歪。createCLAHE(2.0, new Size(8, 8))里的 2.0 是裁剪限幅值越大对比度拉得越猛浅色字越清楚但背景噪点也会被一起放大8×8 是分块网格图越大网格应该越细。这一步在传真件和手机拍照上收益明显在干净的扫描件上反而是负担可以直接跳过。adaptiveThreshold的两个参数是调优重点。blockSize决定多大的邻域算一个局部阈值31 适合 12 到 16 号字字更小就降到 21字更大或版面很空就升到 51它必须是奇数。C是从局部均值里减掉的常数越大越保守背景噪点越少但浅色字越容易丢15 是中间值噪点多的传真件可以调到 20浅色印刷的合同可以降到 8。有一种情况不要二值化低对比度的彩色底纹上印黑字二值化会把底纹和字一起判成黑或者一起判成白。这种图保留灰度只做 CLAHE 增强直接喂给识别引擎效果反而更好。3.3 识别执行层超时、并发、置信度过滤识别是 CPU 密集型任务放进 Web 容器的公共线程池里会拖垮其他接口。做法是独立线程池核心线程数取 CPU 核数的一半队列必须有界。Bean(ocrPool) public ExecutorService ocrPool() { int cores Runtime.getRuntime().availableProcessors(); return new ThreadPoolExecutor( Math.max(2, cores / 2), // 核心线程留给 IO 和 GC 的余量 Math.max(2, cores / 2), 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(64), // 有界队列防止堆积把内存吃光 new ThreadPoolExecutor.AbortPolicy() // 满了直接拒绝由调用方降级 ); } public OcrResult recognize(BufferedImage img) { FutureOcrResult f ocrPool.submit(() - doRecognize(img)); try { // 单页 8 秒上限超过基本可以判定是超大图或引擎卡死 return f.get(8, TimeUnit.SECONDS); } catch (TimeoutException e) { f.cancel(true); return OcrResult.timeout(); // 返回明确状态不要把异常抛到上层 } catch (Exception e) { return OcrResult.failed(e.getMessage()); } }超时值定在 8 秒不是拍脑袋300 DPI 的 A4 页面在 4 核机器上用 Tess4J 大约 1 到 2 秒用 ONNX 模型 0.5 到 1 秒超出 8 秒的一定是异常输入比如分辨率上万像素的扫描大图或者带 20 页的 PDF。对 PDF 加一条页码上限比如只处理前 5 页能挡掉大部分超时。结果对象至少要有四个字段全文文本、逐行文本加行框、整页平均置信度、耗时。行框bbox是后面做版面重排和字段定位的基础一开始就存下来别等用的时候再补。置信度低于 60 的行单独存一个数组前端把这些行高亮出来让业务人员确认这个设计比“提高准确率”更能解决实际问题。4. 参数调优把识别率从六成推到九成的六个旋钮预处理和链路都通了之后剩下的提升全在参数上。这一章把影响最大的几个参数列出来附带我实际调过的取值区间。要说明的是同一组参数在不同来源的图片上表现差别很大所以要按图片类型分组调不要指望一套参数打天下。4.1 DPI 与短边像素先定分辨率再谈算法分辨率是所有参数的前置条件。字高低于 20 像素任何引擎都会大幅掉点因为汉字的偏旁在 20 像素以下根本凑不出笔画。300 DPI 下 10.5 磅的宋体大约对应 43 像素高是比较舒服的区间。输入类型建议渲染 DPI / 缩放目标说明电子版 PDF文字层可提取不走 OCR直接提文字层有文字层却去渲染识别是纯浪费扫描件 PDF300 DPI原件低于 200 DPI 时上采样到 300收益有限但仍有手机拍照短边 1600~2000 像素低于 1200 明显掉点高于 2500 收益递减界面截图原始分辨率不缩放截图本身就是像素对齐的缩放反而糊传真件短边 2000 像素 去噪二值传真件重点是去椒盐噪点判断电子版 PDF 有没有文字层很简单用 PDFBox 的PDFTextStripper抽一次如果抽出来的字符数超过阈值直接返回整条 OCR 链路都不用走。这一步在很多项目里被漏掉导致大量本该毫秒返回的文档跑了几秒钟推理。4.2 二值化窗口与偏移量blockSize 和 C 怎么试这两个参数没有解析解只能网格搜索。我的做法是准备 30 张标注好的人工样本blockSize取 21、31、41、51C取 8、12、15、20跑 16 组用字符准确率编辑距离归一化排序。通常在 31/15 附近有峰值但传真件会往 41/20 偏浅色印刷会往 21/8 偏。要注意blockSize超过图像短边的三分之一时会退化局部窗口里几乎全是背景算出来的阈值失去意义。所以对短边只有 600 像素的小图blockSize别超过 51。二值化不是必选项。有阴影的手机拍照先做背景估计再减除比直接自适应二值化效果好把图片做一次大核的形态学闭运算得到背景用原图减背景再归一化。这套操作在 OpenCV 里就是morphologyEx加subtract加normalize代码不长对不均匀光照的效果比调C好得多。4.3 PSM 与 OEM版面先验比模型更值钱PSM页面分割模式是很多人只设一次就再也不动的参数但它对结果的影响比换语言包大得多。核心逻辑是你比引擎更清楚这张图的版面把这个先验告诉它。PSM 值含义适用场景3全自动分割不知道版面时的默认值多栏文档容易乱序4单列、变宽文本单栏合同、文章正文6单一文本块票面、卡证、表单主体区域7单行文本已经切成单行的字段图8单个词极短的字段如金额、编号11稀疏文本界面截图、海报、零散标注13原始行不做分割需要自己控制行序时用实践里最有效的组合是“先切块再对每块用 7 或 6”。与其让引擎在整页上猜版面不如先用检测模型或投影法把页面切成列和块然后逐块识别。这块内容在最后一章展开。OEM 一般固定用 1仅 LSTM。0 是传统引擎加 LSTM 融合在中文上并没有优势耗时还更长。4.4 语言包、白名单与置信度阈值语言包用chi_simeng而不是单chi_sim主要是为了数字和字母。纯中文模型遇到“¥1,280.00”这种混排时会倾向于把数字也当成汉字部件识别成奇怪的字。多挂一个eng后数字准确率提升明显代价是耗时增加约 30%。白名单参数tessedit_char_whitelist要慎用。它只在“确定这一行只可能包含某类字符”时才有意义比如金额字段只可能是数字和.,¥身份证号只可能是数字加 X。设了白名单之后引擎会把所有形似的字符强行映射到白名单里如果实际内容超出了白名单范围输出的是看起来很像正确结果的错误值——这种错误比识别失败更危险因为下游没有任何信号能发现它。所以白名单只用在二次识别的字段级裁剪图上绝不用在整页。置信度阈值分两级。一级是整页平均低于 50 的页直接进人工队列不再往下走字段抽取二级是行级低于 60 的行标记复核。阈值不要设得太高卡到 75 会把大量本来正确的行也推给人工反而增加工作量。这个数字应该用两周的实际数据回标一次再定。5. 避坑与排查Java 侧调 OCR 最常翻车的五个现场下面这五条都是线上真实会遇到的问题按“现象 → 原因 → 解决”写方便直接对照排。5.1 tessdata 打进 jar 之后就找不到了现象本地 IDE 跑得好好的打成可执行 jar 部署后启动就抛异常提示找不到语言包或者TESSDATA_PREFIX未设置。原因Tess4J 底层是本地库它要的是一个文件系统上的绝对路径不能直接读 jar 包里的 classpath 资源。setDatapath(classpath:/tessdata)这种写法是不生效的。解决启动时把 classpath 里的语言包复制到临时目录再把那个目录路径传给setDatapath。复制逻辑放在PostConstruct里只执行一次并校验目标文件大小是否与资源一致避免上一次复制中断留下半个文件。PostConstruct public void prepareTessdata() throws IOException { Path dir Files.createTempDirectory(tessdata-); try (InputStream in getClass().getResourceAsStream(/tessdata/chi_sim.traineddata)) { Files.copy(in, dir.resolve(chi_sim.traineddata), StandardCopyOption.REPLACE_EXISTING); } this.tessdataPath dir.toAbsolutePath().toString(); }5.2 识别结果中文被空格拆成单个字现象输出是“壹 万 贰 仟 元 整”每个汉字之间插了空格导致后面所有基于字符串的正则全部匹配失败。原因Tesseract 的分词逻辑是按西文词边界设计的中文没有空格它会在每次低置信度的字符切换处插一个空格。即使设了preserve_interword_spaces1也只能减轻不能根除。解决加一层后处理用正则把“汉字 空白 汉字”之间的空白去掉但要保留数字和字母之间的空格否则“¥ 1 280”会被粘成“¥1280”改变语义。private static final Pattern CJK_GAP Pattern.compile((?[\\u4e00-\\u9fa5])\\s(?[\\u4e00-\\u9fa5])); private static final Pattern MULTI_SPACE Pattern.compile([ \\t]{2,}); public static String tidy(String raw) { String s CJK_GAP.matcher(raw).replaceAll(); s MULTI_SPACE.matcher(s).replaceAll( ); return s.replace(\u00A0, ).trim(); // 顺手处理不间断空格 }5.3 容器里 JNA 加载本地库失败现象容器启动后第一次调用识别就报UnsatisfiedLinkError提示找不到libtesseract或者 JNA 的临时库文件无法加载。原因通常有三种基础镜像里没有装本地库容器的/tmp挂载了noexec选项JNA 解压出来的.so无法执行glibc 版本和编译时的目标不一致。解决基础镜像里显式安装本地库不要指望 jar 自带JNA 的解压目录通过-Djna.tmpdir/opt/jna指到一个可执行的路径启动健康检查里加一次“空图识别”的探测把问题暴露在启动阶段而不是第一个真实请求上。另外java.io.tmpdir也最好指到同一个位置两个参数经常需要一起改。5.4 大图 OOM 与线程池打满现象并发上来之后GC 频率陡增接着出现OutOfMemoryError: Java heap space或者大量请求超时。原因BufferedImage的内存占用等于宽 × 高 × 4 字节300 DPI 的 A4 单张就是 35MB预处理里每个Mat又是一份拷贝一条链路走下来同一张图在内存里可能同时存在五六个副本。并发 10 就是几百兆加上堆里还有别的对象默认堆很快就撑不住。解决三件事一起做。限制单页像素上限比如 4000 万像素超出的先降采样所有Mat用完立刻release()用 try-finally 包住绝不依赖 GC并发数硬性限制在 CPU 核数的一半队列有界满了直接拒绝并返回“稍后重试”。堆参数用-XX:MaxRAMPercentage70让容器感知别写死-Xmx。5.5 表格和多栏文档被读成一行流现象两栏排版的 PDF 识别出来左栏第一行和右栏第一行被拼在了一起阅读顺序完全是乱的。原因PSM 3 的自动分割在多栏场景下按水平投影切行同一水平高度上的左右两栏会被认为是同一行。表格同理单元格之间的空白不足以让它识别出列结构。解决不要指望引擎自己还原版面。先用检测模型拿到所有文本框的坐标按中心点 y 坐标聚类成行容差取文本框高度的 0.6 倍左右行内按 x 排序再按“先左栏后右栏”的规则重排。表格另算先用横竖线检测提取单元格再逐格识别。这套逻辑在最后一章给最小实现。6. 进阶落地把「识字」升级成「可入库的字段」识别出文本只是中间结果业务要的是“金额 1280.00、开票日期 2024-03-11、购买方某某公司”这样的字段。从文本到字段这一段靠正则硬怼在简单票面上能work票面一换就崩。6.1 先切块再识别别让引擎猜版面做法是把识别拆成两步检测模型输出所有文本框的四边形坐标然后按几何关系重建成阅读顺序。核心是按 y 聚类成行。// boxes 已按检测模型输出排好含 x、y、w、h public ListListTextBox groupIntoLines(ListTextBox boxes, double toleranceRatio) { boxes.sort(Comparator.comparingDouble(b - b.y b.h / 2.0)); ListListTextBox lines new ArrayList(); for (TextBox b : boxes) { double cy b.y b.h / 2.0; boolean placed false; for (ListTextBox line : lines) { double refCy line.get(0).y line.get(0).h / 2.0; // 容差取行高的 0.6 倍太小会把同一行拆开太大会把相邻行合并 if (Math.abs(cy - refCy) line.get(0).h * toleranceRatio) { line.add(b); placed true; break; } } if (!placed) { ListTextBox nl new ArrayList(); nl.add(b); lines.add(nl); } } lines.forEach(l - l.sort(Comparator.comparingDouble(b - b.x))); return lines; }toleranceRatio取 0.6 是个经验值小于 0.4 时上下标和不同字号的混排会被拆成两行大于 0.8 时行距紧的表格会把相邻两行合并。多栏文档还要在聚类前先按 x 做一次区间切分否则左右栏仍然会串到同一行里。6.2 字段抽取与校验正则兜底 置信度分流有了阅读顺序字段抽取就有了上下文。做法是关键词锚定加正则约束在“金额”“合计”“小写”这类锚点附近的两行内找符合金额格式的串而不是全页扫。金额格式用[¥]?\\s?\\d{1,3}(,\\d{3})*(\\.\\d{1,2})?日期用\\d{4}[-年/]\\d{1,2}[-月/]\\d{1,2}找到后统一转成标准格式再入库。校验这一步别省。金额做一次大小写一致性检查——阿拉伯数字和中文大写能对上才放行对不上就打标日期做一次合法性检查2024-02-31这种必须拦下来税号做长度和校验位检查。这些规则写起来不到一百行能挡掉大部分静默错值。分流策略上我给每个字段算一个综合置信度取该字段所在文本框的识别置信度乘以正则匹配的完整度再乘以校验是否通过。三项都满分的直接入库缺一项的进待复核表附上原图裁剪区域业务人员扫一眼就能确认。这套分流让自动入库率从 40% 提到 85% 左右剩下 15% 才是真正需要人看的。说个我自己踩过的坑早期为了追求自动化率把置信度阈值一路往下调结果错值混进了业务表事后清理数据花的时间远超省下的审核时间。后来把阈值钉死宁可多推一部分给人工也不让错值无声无息地流下去。OCR 这条链路上「识别不出来」是可控的失败「识别错了还通过校验」才是真正的事故。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?