先把 PDF 每页渲染成图再送进去——Xiaomi-OCR-0 处理多页文档的输入路径

文章导读
扫描版 PDF 直接整份送进 Xiaomi-OCR-0,通常不是稳妥路径:PDF 内部可能是矢量文本、扫描位图、混合页面,模型端要么不接受这种容器格式,要么内部按自己的方式解码,你无法确认最终送进去的是哪一版像素。更可控的做法是先用 poppler 的 pdfinfo 和 pdftoppm 把每页渲染成 PNG,再按页序逐张送入模型。渲染 DPI 建议从 200 起步,先看 pdfinfo 给出的
📋 目录
  1. A 用 pdfinfo 读取页数与每页物理尺寸
  2. B 用 pdftoppm 按目标 DPI 逐页渲染 PNG
  3. C 检查渲染结果的页数、宽高与色彩模式
  4. D 按页序循环送入模型并回收结果
A A

扫描版 PDF 直接整份送进 Xiaomi-OCR-0,通常不是稳妥路径:PDF 内部可能是矢量文本、扫描位图、混合页面,模型端要么不接受这种容器格式,要么内部按自己的方式解码,你无法确认最终送进去的是哪一版像素。更可控的做法是先用 poppler 的 pdfinfo 和 pdftoppm 把每页渲染成 PNG,再按页序逐张送入模型。渲染 DPI 建议从 200 起步,先看 pdfinfo 给出的页面物理尺寸和小字号密度,再决定是否上调到 300;先用少量页面(比如正文密集的一页)试渲染,确认尺寸与清晰度后,再批量处理整份文档。

多页扫描 PDF 交给 Xiaomi-OCR-0,建议走“先渲染成单页位图、再逐页送入”的路径,而不是整份 PDF 直接投喂。操作顺序是:用 pdfinfo 确认 Pages 与 Page size 作为渲染基准,用 pdftoppm -r 200 -png 输出按页编号的 PNG,用 identify 或 PIL 核对页数、宽高和色彩模式,最后按文件名排序循环调用模型并把结果写回同序号文件。风险边界在于渲染 DPI 与图像尺寸上限需要结合模型端接受的输入规格确认;彩色扫描件渲染成 RGB 并不等于必须转灰度,是否转要看模型对通道的实际处理方式。

用 pdfinfo 读取页数与每页物理尺寸

渲染前先拿到页面基准,这一步决定后面 DPI 怎么定、页数对不对。poppler 的 pdfinfo 输出里,Pages 是总页数,Page size 是页面物理尺寸,单位通常是 points(1 point = 1/72 英寸)。如果各页尺寸不一致,有的版本会显示 Page size 后跟 (various) 或列出多组尺寸,这时要单独看单页。

pdfinfo input.pdf | grep -E '^(Pages|Page size)'

拿到 Page size 后可以粗略换算渲染像素:宽像素 ≈ 宽 points ÷ 72 × DPI。例如 A4 的 595 points 宽在 200 DPI 下约 1653 像素,在 300 DPI 下约 2479 像素。这个换算是判断“渲染后会不会过大”的依据,比渲染完再回头看尺寸省事。若页数很多但只需抽样,先记下目标页号,后面用 -f/-l 限定范围。

用 pdftoppm 按目标 DPI 逐页渲染 PNG

pdftoppm 会把 PDF 每页渲染成一张位图,输出文件名由你给的前缀加页号组成,这就是后面循环的输入来源。先建目录,再带 -r 指定 DPI、-png 指定格式:

先把 PDF 每页渲染成图再送进去——Xiaomi-OCR-0 处理多页文档的输入路径
mkdir -p pages
pdftoppm -r 200 -png input.pdf pages/page

输出命名规则是“前缀-页号.png”,页号会按总页数的位数补零,比如共几十页时通常得到 page-01.png、page-02.png 这样的文件;页数少的时候可能不补零。补零与否直接影响到第 4 步的排序,如果发现输出是 page-1.png 到 page-10.png,不要用纯字符串排序。只渲染部分页面时加页码范围:

pdftoppm -r 200 -png -f 3 -l 5 input.pdf pages/page

渲染完成后先用 ls pages/ | head 看一眼命名形态,记下来,再决定循环里怎么排序。如果 200 DPI 下小字糊成一团,再调高到 300 重渲染一次,不要在同一份文档里混用不同 DPI 的输出。

检查渲染结果的页数、宽高与色彩模式

这一步防两类问题:漏页,以及灰度或通道异常。先对页数:

先把 PDF 每页渲染成图再送进去——Xiaomi-OCR-0 处理多页文档的输入路径
ls pages/*.png | wc -l

得到的数字应当和 pdfinfo 的 Pages 一致;不一致通常说明用了 -f/-l 限定范围,或者渲染中途出错。再用 ImageMagick 的 identify 看宽高与色彩模式:

identify -format '%f %wx%h %[colorspace] %[channels]\n' pages/*.png

不想依赖 ImageMagick 时,用 PIL 读同样三项。建议把这段放在循环送模型之前当作前置校验:

from pathlib import Path
from PIL import Image

for p in sorted(Path("pages").glob("*.png")):
    im = Image.open(p)
    print(p.name, im.width, im.height, im.mode)

mode 为 RGB 表示三通道彩色,L 表示单通道灰度。扫描件本身是黑白文档、却被渲染成 RGB,这在多数情况下不影响识别,但如果模型端对通道数有固定预期,就要统一转换,操作是 mogrify -colorspace Gray pages/*.png 或用 PIL 的 convert("L") 另存一份。另需留意:如果渲染宽高明显超过模型端可接受的输入尺寸,模型侧可能自行缩放,缩多少要以实际配置为准,不要凭猜测断言无损。

先把 PDF 每页渲染成图再送进去——Xiaomi-OCR-0 处理多页文档的输入路径

按页序循环送入模型并回收结果

多页文档最容易出错的地方不是识别质量,而是结果拼回去时页序乱了。骨架的做法是:按页号数值排序读图,逐页调用,结果文件用与图片相同的序号命名,做到一图一文一一对应。下面只给调度骨架,call_ocr 需要替换成你实际环境的调用方式,具体参数以 Xiaomi-OCR-0 的接入说明为准,不要照抄占位实现。

import re
from pathlib import Path

def call_ocr(image_path):
    # 替换为实际调用:读入 image_path,返回该页文本或结构化结果
    raise NotImplementedError

out_dir = Path("ocr_out")
out_dir.mkdir(exist_ok=True)

pages = sorted(
    Path("pages").glob("*.png"),
    key=lambda p: int(re.search(r"(\d+)", p.stem).group(1)),
)

for p in pages:
    result = call_ocr(p)
    (out_dir / (p.stem + ".txt")).write_text(result, encoding="utf-8")
    print("done", p.name)

排序用正则提取页号再转整数,可以同时兼容 page-01.png 和 page-1.png 两种命名,避免 page-10 排在 page-2 前面。结果文件按 page-01.txt 这样落盘,和原图同名不同后缀,回查某一页的原文或截图时不用再猜。合并成整篇文本时按同样顺序读取:

for f in $(ls ocr_out/*.txt | sort -V); do cat "$f"; echo; done > merged.txt

如果某一页返回空结果,不要静默跳过:在循环里记录文件名和返回状态到一个日志文件,跑完后对照 ls pages/*.png 的清单逐条核对,缺哪页重新渲染该页再送一次,比整份重跑省时间。渲染 DPI、是否转灰度、结果命名规则这三项一旦定下来,建议在脚本里写成常量,避免第二次处理时参数漂移导致两次结果不可比。