纸面数学公式 OCR 的端到端工程:页面级解耦、ONNX 推理与评估边界
本文目录
把一页拍照教材变成可搜索文本,难点不是简单地“再换一个更大的 OCR 模型”。页面里同时有段落、题号、行内公式、独立公式、分数和上下标;如果前置检测把公式切坏,后面的识别器拿到的就已经是残缺图像。这个项目最有代表性的工程决策,是先重新安排整页处理顺序,再分别调用擅长不同任务的模型。
这里讨论的是一个可复现的数学练习页原型,不是面向任意 PDF、手写体、表格、复杂版面或生产 SLA 的通用 OCR 服务。
先定位流程瓶颈
文本检测器通常把页面切成近似横向的文本行。分式的分子、分母、根号和上下标却不一定落在一个这样的矩形里。若先按文字行裁剪,再只在行内寻找公式,公式框可能只剩半个分子,或者整道题被合并进一个大框。后续识别器即使本身很强,也无法从缺失的像素里还原结构。
项目做了输入放大对照:把文本检测输入边长从 960 提高到 1920,框覆盖有所改善,但完整覆盖率仍停在 10/30。它说明这类失败不只是分辨率不足,而是“用文本行的几何先验切数学公式”这个流程假设不成立。
页面级双路流水线
更稳妥的顺序是先在整页图像上找到公式区域,再把文字和公式送去不同识别路径:
原始页面
├─ 整页公式检测 -> 框去重/异常高框切分 -> 原图公式裁剪 -> LaTeX 识别
└─ 公式区域涂白 -> 文本行检测与识别
↓
按几何位置合并阅读顺序
↓
文本 + 数学公式
1. 公式检测先看整页
公式检测器在原始页面上运行,不依赖 DBNet 的行框。输出包含矩形区域、置信度,以及行内公式或独立公式的类型。后处理再清理同位置的跨类别重复框;对异常高的框尝试按连通域和行聚类切分,并在适合的情况下把被分数线拆开的分子、分母重新组合。
报告给出的默认策略包括:跨类 IoU 去重阈值 0.9、检测框高度超过中位数约 2.5 倍时尝试切分。它们是这组页面数据上的可调启发式,不是数学排版的普适规则;换字体、拍摄角度或版面后都应重新验证。
2. 为文本识别制作“去公式图”
得到公式框之后,在副本上把公式区域连同少量边缘涂白,再让 DBNet 查找文字行。默认边缘外扩 6 像素,目的是减少残留笔画干扰。公式识别仍使用原始图像中的裁剪,而不是被涂白的副本,因此两条路径不会互相破坏输入。
也可以选择事后过滤公式覆盖率高的文字框,或完全不处理作为对照。仓库的同一页面对比显示,事后过滤会把整段混合内容一起丢掉;涂白后重新检测,保留的文本行更多。这再次说明:中间表示和处理顺序本身就是模型系统的一部分。
3. 两条路径各自识别
- 普通文本使用 DBNet 定位行框,再由 PP-OCRv4 输出文本。
- 公式使用 YOLOv8 MFD 定位,再由 UniMERNet-tiny 输出 LaTeX。
- 每条公式独立识别;单个裁剪失败会留下错误信息,不必让整页流程直接中止。
- 识别结果保留区域、置信分数和类型,便于回看是哪一步出了问题。
当前仓库把检测、识别、几何后处理和页面编排分成独立模块,因此可分别关闭某条路径、观察中间框或替换运行提供者。调试输出会保留原图/涂白图检测差异、公式框去重和切分记录、分阶段耗时及每条公式的识别结果。
4. 重新合并阅读顺序
两路结果最后根据矩形框聚成视觉行,行内从左向右排序,再按行的纵坐标输出。导出 Markdown 时,行内公式使用 $...$,独立公式使用 $$...$$,还会依据行间距插入段落空行。它是针对常规横排教材页的几何近似,不理解语义段落,也不等同于版面分析模型。
多栏、倾斜或旋转页面、跨栏公式、复杂表格、脚注和阅读方向不同的文档,都可能让这种简单排序失效。需要这些场景时,应增加版面区域检测与方向估计,而不是只调大框阈值。
为什么选择 ONNX Runtime
项目将文本检测/识别、公式检测和公式识别模型以 ONNX 格式运行,避免推理阶段依赖完整的训练框架。公式解码器拆为 encoder 与自回归 decoder;可选的 KV cache 路径用 prefill 加逐 token step,避免每次都重算全部历史 token。若模型文件缺失,代码会明确报出相应路径,权重本身没有随仓库提交,项目报告估算完整模型资产约 1.1 GB。
公式 tokenizer 优先使用轻量 tokenizer 库;没有时才尝试基于 transformers 的回退实现。因此更准确的描述是“推理图由 ONNX Runtime 执行”,不能把它说成整个 Python 环境完全不依赖其它运行库。CUDA 只有在当前环境提供相应 Execution Provider 时才会启用;否则会回落到 CPU。
报告里的数字应该怎样读
仓库的精度报告针对一张约 3000×4000 的真实教材照片做了分环节实验。在报告列出的十道公式对照中,页面级解耦流程将公式区域完整覆盖从 10/30 提高到 30/30;公式语义相似度从 51.2% 提高到 93.7%,其中 9/10 达到 80%。另一个独立的裁剪识别实验只有 9 个公式样本;文本识别样本也只有 6 行。
这些数字是特定样本、特定模型版本和特定语义归一化方法的项目记录,不是跨教材、字体、拍照设备的总体准确率。公式 LaTeX 不能只用逐字符相等评估:\mathbf、空格和大括号的排版差异可能不改变数学语义,但也不能因此忽略漏项、错误上下标或运算符替换。项目使用文本归一化并辅以渲染比对,这比只报字符准确率更贴近阅读结果。
还要留意报告口径:README/流水线注释与详细精度报告对旧流程的公式区域平均可用率分别出现 43.9% 和 44.8%,测试集合也分别写到 31 个与 30 个公式。发布性能结论前应统一样本清单和计算脚本,不能挑一个更好看的数字当作普遍结论。
性能与上线边界
仓库在 CPU 上记录的整页处理耗时约为 115–170 秒,其中 30 个公式的自回归识别占 100–160 秒。KV cache 在逐 token 输出一致性验证中通过,但 CPU 墙钟时间几乎不变,主要因为 decoder 权重读取受内存带宽限制;报告中的 INT8 动态量化约有 1.35 倍加速,却改变了部分样本的 token 输出,因此默认没有打开。
这组耗时不适合直接塞进同步网页请求,更不适合当成“上传即刻完成”的在线演示。仓库当前提供的是 Python 推理流水线与模型导出/验证脚本,README 明确说明旧 Web 服务链路已移除,并没有现成的受限上传、排队、超时、取消、文件清理和权限 API。模型权重也不在仓库里。
要做线上服务,至少需要单独的 GPU/推理主机或明确的长任务队列、图片大小和页数限制、隔离临时文件、数据保留策略、模型许可核查、进度查询与失败重试。把仓库里的本地 Python 函数包一层 HTTP,并不会自动得到可安全运营的在线 OCR 产品。
复现与扩展路线
在具备模型文件和依赖的环境中,可以从单张页面脚本入口开始,再使用验证目录中的脚本逐项检查文本、公式、框切分、页面解耦、KV cache 和中间结果。先固定测试图像与标注,记录模型文件校验值、运行库版本、执行提供者和整页耗时;修改检测阈值后重新跑同一份对照集。
下一步优先级可以是:扩大并分层标注真实测试集;把报告数字统一由脚本生成;加入多栏/旋转页面的版面模型;为单公式失败建立可读的错误分类;最后再比较 GPU 批处理、量化和更大公式模型。每次优化都应同时报告准确性、完整覆盖率和延迟,避免只提高识别分数却把处理时间推到不可用。
这个项目体现的工程方法
- 先用真实图像和中间框定位错误,不把所有问题都归因于“模型不够大”。
- 通过输入放大与流程解耦的对照实验区分分辨率问题和几何先验问题。
- 让文本和公式各走适合自己的模型,再用明确的空间规则合并结果。
- 保存可检查的中间产物,并把量化、KV cache 等性能改动与输出一致性一起验证。
- 把小样本评估、CPU 延迟、缺失模型文件和服务端缺口写出来,而不把实验原型包装成线上能力。
源仓库
本文是基于项目代码和仓库报告重新组织的工程说明,不是模型权重、第三方模型或整仓文件的镜像。查看完整实现、导出脚本和验证报告,请访问 Paper OCR 源仓库。上游模型和运行库的许可、使用范围及发布要求,应以各自仓库的当前声明为准。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。