PAYATHON 2026

如何使用本地开源工具将含手写内容和复选框的扫描 PDF 转为 JSON?

技术老齐

可以用本地开源工具实现。如果只有一种表单或少量固定版式,建议先对齐页面,再按字段裁剪,分别识别文字和复选框,最后映射到固定字段。可以优先在 CPU 上运行。

字段配置、页面对齐和结果校验仍需自己实现,通用 PDF 转文本工具通常无法直接完成这些工作。手写识别还要根据实际语言和字迹选择模型,无法保证任意表单都能自动识别正确。

先判断表单版式是否固定

“无法修改表单布局”和“每份表单布局都不同”是两种情况,实施成本差别很大:

  • 所有文件都使用同一版表单时,可以配置一次字段位置,对齐后批量提取。
  • 有几种固定版本时,可以先识别版本,再选择对应模板。
  • 每份表单的问题和排版都不同时,还需要做版面分析、关联问题与答案,并将问题映射到固定 key。仅靠坐标配置无法解决。

前两种情况不必先引入本地 LLM。直接在配置中写好字段与 key 的对应关系,识别程序只需读取相应区域。

纯扫描 PDF 中的复选框已经变成像素,通常没有可直接读取的 PDF 表单控件状态。

可以使用哪些开源工具

环节工具使用范围
PDF 转图片Poppler 的 pdftoppm将每页渲染为图片
页面对齐、裁剪、复选框分析OpenCV图像配准、阈值分割和形态学处理
印刷体识别Tesseract,配合 pytesseract读取题目、标签和印刷文字
手写识别TrOCR,或适合实际语言的 PaddleOCR 模型读取裁剪后的答案区域,效果需要实测
固定结构输出Python json,可配合 JSON Schema固定 key、检查类型和必填字段

Tesseract 官方 FAQ 说明,它主要面向印刷体,手写识别效果有限,不建议把所有答案都交给它处理。Tesseract 手写识别说明

手写识别可以先试用以下模型:

  • microsoft/trocr-base-handwritten:模型卡标注为 MIT 许可证,适合单行文字图片,在英文手写 IAM 数据集上微调。它不能直接解析整页表单,也不应默认用于中文手写。模型说明、IAM 数据集说明
  • PaddleOCR 的 PP-OCRv5:官方文档列出了中文、英文手写等识别场景,可以作为相应语言表单的候选。能否识别好具体字段,仍需用真实样本验证。PP-OCRv5 官方说明

如果复选题占比较高,也可以参考采用 MIT 许可证的 OMRChecker。它用于识别答题卡标记。用于普通业务表单前,需要验证框型、定位方式和模板配置是否适用,手写文本仍需另行识别。

没有角落标记,怎样对齐页面

可以用表单原有的印刷内容定位,例如标题、表格交点、固定说明文字或线条,不一定要添加标记。

具体可以这样做:

  1. 选择一张清晰的参考页,最好是空白表单。
  2. 在参考页上标注每个答案区域和复选框位置。
  3. 用 OpenCV 在扫描页与参考页之间匹配特征点。
  4. 用 RANSAC 排除错误匹配,估计变换,再将扫描页映射到参考页坐标。
  5. 检查对齐结果,通过后再裁剪字段。

OpenCV 的 findHomography() 和 warpPerspective() 可以用于这一流程。官方教程介绍了特征匹配与使用 RANSAC 求单应矩阵的方法,用于表单时还需补充页面质量检查。OpenCV 官方教程

匹配时尽量使用固定印刷区域,避开填写内容。重复表格容易造成错误匹配。除了检查是否算出了变换矩阵,还应检查匹配点分布、内点情况和关键区域的对齐误差。

整页透视变换也有局限。纸张弯曲、局部拉伸或页面边缘缺失时,可能需要局部校正或人工处理。对齐失败的页面应进入复核队列,继续按固定坐标裁剪很容易读错字段。

复选框应怎样识别

复选框状态可以单独按 OMR(Optical Mark Recognition,光学标记识别)问题处理。

对于位置固定、扫描质量较稳定的方框,可以先用像素占比法:

  1. 裁剪复选框内部,排除印刷边框。
  2. 将图像二值化。
  3. 统计内部深色像素占比。
  4. 根据样本校准“未选中”和“已选中”的阈值。

建议保留三种状态:

{
  "consent": true,
  "receive_updates": false,
  "other_option": null
}

null 表示无法可靠判断。如果业务最终只接受布尔值,可以在人工复核后再输出 true 或 false。

很淡的勾、伸出框外的勾、擦除痕迹和划掉重选,都可能让像素占比法误判。若这些情况较多,可以用标注样本训练局部图像分类器,并保留“待复核”类别。仅增加一个阈值,无法适应所有填写习惯。

固定 key 应在识别之前定义

不要直接用 OCR 识别出的题目文字作为 JSON key,否则题目中的一个识别错误就可能改变字段名。

可以预先维护这样的配置:

{
  "schema_version": "form_v1",
  "fields": {
    "applicant_name": {
      "type": "handwriting",
      "page": 1,
      "bbox": [350, 420, 1100, 510]
    },
    "reference_code": {
      "type": "printed",
      "page": 1,
      "bbox": [350, 540, 850, 610]
    },
    "consent": {
      "type": "checkbox",
      "page": 1,
      "bbox": [242, 622, 268, 648]
    }
  }
}

这里的坐标只是示意,采用 [x1, y1, x2, y2] 格式,必须根据统一尺寸的参考页重新标注。复选框的 bbox 特指框内区域。

输出时始终遍历同一份字段配置,即使答案为空或识别失败,也能保持 key 一致。缺失值可以统一使用 null,另外记录复核原因。

单选题应检查是否最多选中一项;多选题可以为每个选项保留一个固定布尔字段。

Python 示例:从已对齐页面生成 JSON

先用 Poppler 渲染 PDF,可以从 300 DPI 开始:

pdftoppm -r 300 -png input.pdf page

渲染后需要完成页面对齐。以下示例读取已对齐并保存为 aligned.png 的页面,演示字段裁剪、识别和 JSON 输出,不包含模板匹配与配准代码。

安装 Python 依赖:

pip install numpy opencv-python pillow pytesseract torch transformers

还需要单独安装 Tesseract 可执行程序和相应语言数据,pytesseract 只是 Python 封装。pytesseract 安装说明

部署时先将模型下载到本地目录,这一步不需要接触表单数据。离线机器可以复制完整模型目录:

from huggingface_hub import snapshot_download

snapshot_download(
    repo_id="microsoft/trocr-base-handwritten",
    local_dir="./models/trocr-base-handwritten",
)

实际处理时只读取本地文件。示例中的手写字段假设为英文单行答案:

import json
from pathlib import Path

import cv2
import numpy as np
import pytesseract
import torch
from PIL import Image
from transformers import TrOCRProcessor, VisionEncoderDecoderModel

FIELDS = {
    "applicant_name": ("handwriting", (350, 420, 1100, 510)),
    "reference_code": ("printed", (350, 540, 850, 610)),
    # 此坐标仅覆盖复选框内部,不包含印刷边框
    "consent": ("checkbox", (242, 622, 268, 648)),
}

# 仅为演示值,必须用本批表单的标注样本重新校准
EMPTY_MAX = 0.02
CHECKED_MIN = 0.12

model_dir = "./models/trocr-base-handwritten"
processor = TrOCRProcessor.from_pretrained(
    model_dir, local_files_only=True
)
model = VisionEncoderDecoderModel.from_pretrained(
    model_dir, local_files_only=True
).to("cpu").eval()

page = Image.open("aligned.png").convert("RGB")
gray = cv2.cvtColor(np.asarray(page), cv2.COLOR_RGB2GRAY)
_, ink = cv2.threshold(
    gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU
)

answers = {}
needs_review = []
checkbox_scores = {}

for key, (kind, box) in FIELDS.items():
    x1, y1, x2, y2 = box
    if not (0 <= x1 < x2 <= page.width
            and 0 <= y1 < y2 <= page.height):
        raise ValueError(f"Invalid bbox for {key}: {box}")

    crop = page.crop(box)

    if kind == "checkbox":
        region = ink[y1:y2, x1:x2]
        score = float(np.count_nonzero(region) / region.size)
        checkbox_scores[key] = score

        if score <= EMPTY_MAX:
            value = False
        elif score >= CHECKED_MIN:
            value = True
        else:
            value = None

    elif kind == "handwriting":
        pixels = processor(
            images=crop, return_tensors="pt"
        ).pixel_values
        with torch.inference_mode():
            ids = model.generate(pixels, max_new_tokens=64)
        value = processor.batch_decode(
            ids, skip_special_tokens=True
        )[0].strip() or None

        # 示例尚未建立手写自动验收规则,先要求复核
        needs_review.append(key)

    else:
        value = pytesseract.image_to_string(
            crop, lang="eng", config="--psm 7"
        ).strip() or None

    answers[key] = value
    if value is None and key not in needs_review:
        needs_review.append(key)

result = {
    "schema_version": "form_v1",
    "answers": answers,
    "needs_review": needs_review,
    "checkbox_scores": checkbox_scores,
}

Path("result.json").write_text(
    json.dumps(result, ensure_ascii=False, indent=2),
    encoding="utf-8",
)

--psm 7 适用于单行文字区域,多行答案需要采用合适的分割方式。使用这个 TrOCR 检查点时,应先将答案裁剪成单行再识别。

代码中的 checkbox_scores 记录深色像素占比,并非识别正确率。文字识别部分也尚未实现完整的置信度和业务校验,即使输出了非空字符串,内容仍可能有误。

批量运行前需要补齐什么

先挑选一批有代表性的页面,人工标注正确答案,样本应覆盖空白、浅色笔迹、涂改、歪斜和不同书写者。分别检查字段定位、文字识别和复选框判断是否准确,再决定哪些结果可以自动通过。

批量处理还需要考虑以下问题:

  • 单独验证空白检测。手写生成模型对空白区域也可能输出文字,有输出不代表确实填写过。
  • 为日期、编号和枚举答案添加格式校验,但格式合法仍不代表内容正确。编号应保留字符串类型,避免丢失前导零。
  • 保留来源文件标识、页码和字段坐标,方便复核时回到原图。诊断截图和日志也可能包含个人数据,需要按原始文件的要求管理。
  • 多页表单应先确定页面归属和页序,再合并为一份 JSON,避免将不同填写者的页面拼在一起。

CPU 可以承担页面预处理、复选框分析和文字推理,实际吞吐量需要测量。如果手写识别成为瓶颈,再用 NVIDIA 显卡加速。8 GB 显存能否满足需求,取决于模型、精度和批大小,不能仅凭显存容量保证。

处理期间可以通过本地模型路径、local_files_only=True 和 HF_HUB_OFFLINE=1 使用 Hugging Face 的离线加载方式。如果要求整个进程完全无网络访问,还可以在断网环境中运行。离线运行说明

如果版式变化很多,可以尝试用本地视觉语言模型辅助提取,固定 key、字段类型和异常复核仍应由外部代码控制。合法 JSON 只能说明输出的语法可解析,不能证明答案与原始表单一致。

备注:内容仅供参考。