如何使用本地开源工具将含手写内容和复选框的扫描 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。它用于识别答题卡标记。用于普通业务表单前,需要验证框型、定位方式和模板配置是否适用,手写文本仍需另行识别。
没有角落标记,怎样对齐页面
可以用表单原有的印刷内容定位,例如标题、表格交点、固定说明文字或线条,不一定要添加标记。
具体可以这样做:
- 选择一张清晰的参考页,最好是空白表单。
- 在参考页上标注每个答案区域和复选框位置。
- 用 OpenCV 在扫描页与参考页之间匹配特征点。
- 用 RANSAC 排除错误匹配,估计变换,再将扫描页映射到参考页坐标。
- 检查对齐结果,通过后再裁剪字段。
OpenCV 的 findHomography() 和 warpPerspective() 可以用于这一流程。官方教程介绍了特征匹配与使用 RANSAC 求单应矩阵的方法,用于表单时还需补充页面质量检查。OpenCV 官方教程
匹配时尽量使用固定印刷区域,避开填写内容。重复表格容易造成错误匹配。除了检查是否算出了变换矩阵,还应检查匹配点分布、内点情况和关键区域的对齐误差。
整页透视变换也有局限。纸张弯曲、局部拉伸或页面边缘缺失时,可能需要局部校正或人工处理。对齐失败的页面应进入复核队列,继续按固定坐标裁剪很容易读错字段。
复选框应怎样识别
复选框状态可以单独按 OMR(Optical Mark Recognition,光学标记识别)问题处理。
对于位置固定、扫描质量较稳定的方框,可以先用像素占比法:
- 裁剪复选框内部,排除印刷边框。
- 将图像二值化。
- 统计内部深色像素占比。
- 根据样本校准“未选中”和“已选中”的阈值。
建议保留三种状态:
{
"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 只能说明输出的语法可解析,不能证明答案与原始表单一致。
备注:内容仅供参考。
