2952 字
约 9 分钟
18
S27-X21 · OCR 与文档解析工程:版面分析、识别与后处理
把一页扫描件丢进 OCR,拿到一坨字符,然后发现下游抽不出任何字段——这是最典型的失败。识别率高不等于可用,因为结构丢了。下面按链路拆每一环的产出、失败模式与验收口径。
1. 完整链路
采集 → 预处理 → 版面分析 → 文本识别 → 表格/公式还原 → 后处理 → 结构化
(扫描/PDF/拍照) (去噪/纠偏/二值化) (区域+阅读顺序) (行级转写) (单元格/LaTeX) (纠错/合并) (字段+JSON)
| 环节 | 输出 | 主要失败模式 |
|---|---|---|
| 预处理 | 规整位图 | 过度二值化吃掉细笔画 |
| 版面分析 | 区域框 + 类型 + 顺序 | 双栏读序错、表格框漏 |
| 文本识别 | 字符序列 + 置信度 | 小字丢失、字体失配 |
| 表格还原 | 单元格矩阵 | 合并单元格错位 |
| 后处理 | 修正文本 | 纠错引入新错 |
| 结构化 | JSON 字段 | 字段错配、单位丢失 |
2. 为什么 OCR 不等于文档理解
OCR 回答"这页上有什么字",文档理解回答"这些字在什么位置、属于哪个字段"。
| 层级 | 问题 | 只做 OCR 的后果 |
|---|---|---|
| 字符层 | 这个字是什么 | 基本可用 |
| 行层 | 行的边界在哪 | 双栏拼成一行,语义错乱 |
| 区域层 | 这是标题还是页脚 | 正文与页脚混在一起 |
| 结构层 | 哪格属于哪列 | 金额错列,算错账 |
| 语义层 | 这是合计还是单价 | 字段错配 |
结论:识别准确率是必要条件,不是充分条件;下游要字段,字段依赖结构。
3. 版面分析:区域类型与阅读顺序
{"page": 3, "regions": [
{"id": "r1", "type": "title", "bbox": [72, 60, 520, 96], "order": 0},
{"id": "r2", "type": "text", "bbox": [72, 110, 300, 640], "order": 1, "col": 1},
{"id": "r3", "type": "text", "bbox": [320, 110, 540, 640], "order": 2, "col": 2},
{"id": "r4", "type": "table", "bbox": [72, 660, 540, 900], "order": 3},
{"id": "r5", "type": "footer", "bbox": [72, 950, 540, 980], "order": 4}
]}
| 区域类型 | 下游用途 | 判错代价 |
|---|---|---|
| title / heading | 章节树 | 目录错乱 |
| paragraph | 正文抽取 | 顺序错则语义反转 |
| table | 结构化 | 错行错列 |
| figure + caption | 图文关联 | 图注丢失 |
| header / footer | 通常剔除 | 页码混进正文 |
| seal / stamp | 合规校验 | 误判签署状态 |
阅读顺序的三个难点:
| 难点 | 表现 | 处理 |
|---|---|---|
| 双栏 | 按 y 排序会串栏 | 先聚栏,栏内再按 y |
| 混排 | 通栏标题 + 双栏正文 + 跨栏表格 | 按 x 覆盖范围判断是否跨栏 |
| 表格跨页 | 第 1 页表头在第 2 页续 | 跨页表头继承 |
4. 文字识别的三个关键因素
| 因素 | 具体表现 | 工程对策 |
|---|---|---|
| 图像质量 | 分辨率、噪点、压缩伪影、透背 | 300 DPI 量级重采样、去噪、按需超分 |
| 语言与字体 | 中英混排、繁体、手写体、特殊符号 | 语言模型切换、字典扩充 |
| 方向与倾斜 | 旋转 90°/180°、拍摄倾斜、弯曲 | 方向分类 + 透视矫正 |
小字丢失最常见:整页缩到 1000px 宽再识别,8pt 字只剩几个像素高。正确做法是先裁区域,再按原始分辨率识别。
# PP-OCR 三段式:检测 → 方向分类 → 识别(命令与参数名按所装版本核对)
python tools/infer/predict_system.py \
--image_dir ./pages/ \
--det_model_dir ./models/det \
--cls_model_dir ./models/cls \
--rec_model_dir ./models/rec \
--rec_char_dict_path ./models/ppocr_keys_v1.txt \
--use_angle_cls True \
--drop_score 0.5 \
--output ./out/
| 段 | 作用 | 输出 | 调参点 |
|---|---|---|---|
| 检测(DB 类) | 找文字行框 | 四点/多边形框 | 阈值过松 → 粘连成块;过紧 → 断行 |
| 方向分类 | 判 0/90/180/270 | 角度标签 | 漏判 → 整行乱码 |
| 识别(CRNN/SVTR 类) | 行图转字符 | 文本 + 分数 | 字典不含的字必错 |
PP-OCR 的中文适配点:中文字典覆盖常用汉字与标点、检测对竖排与窄间距做过适配、识别对中英混排做过训练。字典之外的生僻字、异体字一定会错,靠后处理白名单兜。
5. 表格还原:两条路线
| 路线 | 做法 | 优点 | 失败模式 |
|---|---|---|---|
| 结构识别模型 | 直接预测单元格与行列关系 | 无线框表、合并单元格也能处理 | 复杂表头(多级)易错;大表推理慢 |
| 规则线框检测 | 形态学找横竖线 → 求交点 → 切格 | 快、可解释、对有框表极稳 | 无框表、断线、扫描歪斜直接崩 |
实务上两条都跑,结果一致才自动通过,不一致进人工复核。判断依据是线框完整度:
def line_ratio(binary_img):
h = cv2.morphologyEx(binary_img, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT, (40, 1)))
v = cv2.morphologyEx(binary_img, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT, (1, 40)))
return (h.sum() + v.sum()) / binary_img.size / 255 # 经验阈值约 0.02~0.1,须按样本核对
合并单元格是头号难题:rowspan/colspan 判错会让后续所有列连带错位。所以表格结果必须带每个单元格的 bbox 与 span,便于回溯。
6. 公式与手写
| 类型 | 输出形态 | 难点 | 验收 |
|---|---|---|---|
| 印刷公式 | LaTeX | 上下标、分式、矩阵 | 渲染回图比像素 |
| 手写体 | 文本或 LaTeX | 连笔、涂改 | 人工抽检 |
| 化学式/单位 | 保留原文 | 特殊符号 | 字段级校验 |
公式验收的实用技巧:把识别出的 LaTeX 渲染回图片,与原图做结构相似度比较;文本级 BLEU 对公式几乎无意义。
7. 后处理:真正决定可用性
| 动作 | 目的 | 风险 |
|---|---|---|
| 错字纠正 | 修形近字常错对 | 过度纠正改掉专有名词 |
| 繁简统一 | 统一入库口径 | 法律文本的繁体原文被改 |
| 断行合并 | 段落还原 | 列表项被错误合并 |
| 页眉页脚剔除 | 去噪声 | 页码是必要字段时被删 |
| 全半角/空格规整 | 便于匹配 | 代码或编号被破坏 |
| 数字与单位校验 | 防金额错 | 校验规则写错反而误杀 |
postprocess:
normalize: [fullwidth_to_halfwidth, strip_trailing_space]
merge_lines:
enable: true
join_when: "line_end 不以句末标点结尾 且 下一行首字符非列表符号"
keep_hyphen: false
drop_regions: [header, footer]
correct:
dict: ./dict/domain_terms.txt # 领域白名单优先于通用纠错
protect: ["[A-Z]{2,}-\\d+", "\\d+(\\.\\d+)?\\s*(元|万元|%)"]
traditional_to_simplified: false # 合同类文档默认关闭
8. 评测:字符准确率不够
CER 只回答"字对不对",不回答"字段能不能用"。
| 指标 | 口径 | 局限 |
|---|---|---|
| CER / 字符准确率 | 1 - 编辑距离/字符数 | 一个错字不影响字段可用性 |
| 行级准确率 | 整行全对占比 | 严格但仍是字符视角 |
| 区域类型准确率 | 版面分类正确率 | 不反映顺序错 |
| 阅读顺序准确率 | 相邻区域对顺序正确率 | 需要人工标注顺序 |
| 表格单元格准确率 | 行列坐标 + 内容都对 | 标注成本高 |
| 字段抽取通过率 | 下游 schema 校验通过的比例 | 最贴近业务 |
验收建议:以字段抽取通过率为主指标,CER 只作辅助监控。上线前必须单独跑一批低质量样本(歪斜、低分辨率、盖章遮挡),否则印刷体上好看的数字毫无意义。
9. 与 LLM 的配合
正确姿势:先把版面结构化成带标签的文本,再喂给模型抽字段;不要整页图直接丢进去。
| 做法 | 优点 | 问题 |
|---|---|---|
| 整页图直接给多模态模型 | 省事 | 小字丢、表格对齐不稳、成本高、不可复算 |
| 结构化文本 + 模型抽取 | 可复算、可校验、便宜 | 依赖前置解析质量 |
| 结构化文本 + 规则抽取 + 模型兜底 | 稳定且可控 | 需要维护规则 |
prompt = f"""你是单据抽取器。只输出 JSON,字段缺失填 null,不要解释。
字段:invoice_no, date, amount, tax, seller
规则:amount 为价税合计;日期统一 YYYY-MM-DD。
<document>
{structured_text} # 已带区域标签: [TABLE:1] [CELL r1c3] ...
</document>"""
10. 工程要点
pipeline:
ingest: {max_pages: 200, dpi: 300, accept: [pdf, jpg, png, tiff]}
batch: {page_concurrency: 8, doc_concurrency: 2, queue: redis}
retry: {max_attempts: 3, backoff: [2, 8, 30], retry_on: [timeout, empty_result]}
cache:
key: "sha256(file) + parser_version + model_version"
ttl: 30d
review_queue:
trigger: "field_pass_rate < 0.9 or avg_conf < 0.75"
sample_rate: 0.02 # 即便通过也抽检
ui: {side_by_side: true, edit_fields: true, feedback_to: dataset}
| 要点 | 关键决定 |
|---|---|
| 批量与并发 | 并发按页还是按文档,决定内存峰值 |
| 失败重试 | 只重试幂等阶段,避免重复计费 |
| 人工复核 | 触发条件要可解释,否则队列爆 |
| 缓存 | key 须含解析器与模型版本,否则换模型拿到旧结果 |
11. 排错:六种典型故障
| 现象 | 原因 | 处理 |
|---|---|---|
| 小字大面积丢失 | 整页缩图后识别 | 按区域裁切,原始分辨率识别 |
| 整行乱码 | 未做方向分类或漏判 | 打开 use_angle_cls,检查旋转样本 |
| 双栏文本串行 | 按 y 坐标全局排序 | 先聚栏再栏内排序 |
| 表格金额错列 | 合并单元格判错 | 保留 bbox 与 span,复核 |
| 专有名词被"纠正" | 通用纠错覆盖领域词 | 领域白名单优先 |
| 通过率虚高 | 只测印刷体干净样本 | 加低质量样本集 |
12. 边界:这些情况该换方案
| 场景 | 纯 OCR 管线的局限 | 更合适的做法 |
|---|---|---|
| 有原生文本层的 PDF | 不必识别 | 直接抽文本层 + 版面坐标 |
| 手写为主 | 识别率不稳 | 人工录入 + 模型辅助 |
| 极低质量传真件 | 前置修复收益有限 | 退件重扫 |
| 只需关键字段 | 全量转写浪费算力 | 定向区域识别 |
| 需要语义理解 | OCR 不懂语义 | 结构化文本 + 模型抽取 |
一句话:OCR 的产出是文字,文档解析的产出是能被程序消费的结构。前者做对只是起点。
13. 自测
- 识别率 98% 的管线,为什么下游仍可能抽不出字段?
- 双栏文档按 y 坐标排序会出什么问题?正确顺序怎么定?
- PP-OCR 三段式各段的调参点分别是什么?
- 表格还原的两条路线各在什么表上失效?
- 为什么字段抽取通过率比 CER 更适合做主指标?
- 缓存 key 为什么必须包含解析器与模型版本?
S27-X21 · OCR 与文档解析工程:版面分析、识别与后处理
https://www.clxhxhhr.top/posts/6004/ 评论
0 条
还没有评论,先写一条吧。