主题
字号
CHAPTER 08 ≈ 18 MIN READ

OCR、扫描件与文字层

8.1 三种"PDF":你以为的 vs 实际的

8.1.1 从外观看不出区别

打开两份 PDF,肉眼看去完全一样——都是一页白纸上的黑字。但如果你尝试 Ctrl+A 全选文字:

这两份 PDF 的内部结构完全不同:

类型 内部结构 可搜索 可复制 典型来源
原生文字 PDF 内容流有文字绘图指令 Word/LaTeX/Typst 导出
图片 PDF 每页就是一张大图片 扫描仪直出
OCR 增强 PDF 图片底层 + 透明文字层 OCR 处理后

8.1.2 图片 PDF 的内部长什么样

一份扫描件 PDF 的页面对象通常极其简单:

% 页面内容流
q
595.276 0 0 841.89 0 0 cm    % 变换矩阵:把图片拉伸到整个页面
/Im1 Do                       % 画图片对象 Im1
Q

就这么三行。整页内容就是"画一张图"。没有文字操作符(BT/ET/Tj),没有字体对象,没有 CMap。

图片对象本身是一个巨大的 stream:

<< /Type /XObject
   /Subtype /Image
   /Width 2480
   /Height 3508
   /ColorSpace /DeviceGray
   /BitsPerComponent 8
   /Filter /DCTDecode      % JPEG 压缩
   /Length 185432
>>
stream
[185432 字节的 JPEG 数据]
endstream

2480×3508 像素,正好是 300 DPI 下 A4 纸的扫描分辨率。


8.2 OCR 的原理

8.2.1 什么是 OCR

OCR(Optical Character Recognition,光学字符识别)——让计算机"看"图片中的文字,识别出对应的字符。

OCR 的基本流程:

图像输入 → 预处理 → 文字检测 → 字符分割 → 字符识别 → 后处理
  1. 预处理:去噪、二值化、倾斜校正、去除边框/水印
  2. 文字检测:找到图片中哪些区域有文字(区分文字/图表/空白)
  3. 字符分割:把文字区域切分成单个字符(或词)
  4. 字符识别:对每个字符分类(这是"A"还是"B"还是"人"还是"大"?)
  5. 后处理:利用语言模型纠正识别错误("teh" → "the")

8.2.2 现代 OCR 引擎

引擎 类型 精度 速度 中文支持
Tesseract 5 开源(Google 维护) ★★★★☆ ★★★☆☆
PaddleOCR 开源(百度) ★★★★★ ★★★★☆ ★★★★★
Google Cloud Vision 云服务 ★★★★★ ★★★★★ ★★★★★
Azure AI Vision 云服务 ★★★★★ ★★★★☆ ★★★★☆
ABBYY FineReader 商业软件 ★★★★★ ★★★☆☆ ★★★★☆

现代 OCR 的核心模型是基于深度学习的——Tesseract 5 使用 LSTM 网络,PaddleOCR 使用 CRNN + Attention 架构。在清晰印刷体上,识别率可以达到 99%+。

但对于手写体、低质量扫描、复杂版面(多栏、表格、公式混排),精度仍然是一个挑战。

8.2.3 一段 OCR 的实际代码

用 Tesseract 处理一张图片:

import pytesseract
from PIL import Image

# 加载图片
img = Image.open("scanned_page.png")

# 识别文字
text = pytesseract.image_to_string(img, lang='chi_sim+eng')
print(text)

# 获取详细信息(每个字的位置和置信度)
data = pytesseract.image_to_data(img, lang='chi_sim+eng', 
                                  output_type=pytesseract.Output.DICT)

for i in range(len(data['text'])):
    if data['conf'][i] > 60:  # 置信度 > 60%
        print(f"'{data['text'][i]}' at ({data['left'][i]}, {data['top'][i]}) "
              f"conf={data['conf'][i]}%")

输出的关键信息:文字内容 + 位置坐标 + 置信度。这三样东西正好是生成 OCR PDF 文字层所需的。


8.3 OCR 增强 PDF:给图片加上"隐形文字"

8.3.1 文字层的原理

OCR 增强 PDF 的核心思想:在扫描图片上方叠加一层透明的文字

页面结构:
┌─────────────────────────┐
│  透明文字层(可搜索)    │  ← 用户 Ctrl+F 搜索的目标
│  ┌───────────────────┐  │
│  │  扫描图片(可见)  │  │  ← 用户实际"看到"的内容
│  └───────────────────┘  │
└─────────────────────────┘

内容流的结构:

% 先画图片(底层)
q
595.276 0 0 841.89 0 0 cm
/Im1 Do
Q

% 再画透明文字(上层)
BT
3 Tr                          % Text Rendering Mode 3 = 不可见
/F1 12 Tf
72.5 750.3 Td
(This is the recognized text) Tj
ET

关键是 3 Tr——Text Rendering Mode 3 表示"既不填充也不描边",即文字完全不可见。但它在逻辑上存在于那个坐标位置,所以搜索和选择都能命中。

8.3.2 为什么 OCR 层的位置必须精确

如果 OCR 识别出 "Hello" 在图片中的位置是 (150, 200) 到 (250, 220),那么文字层中的 "Hello" 也必须精确放在同样的坐标范围。否则:

好的 OCR 工具会为每个词精确计算字号和字间距,让透明文字的边界和图片中的文字完全重合。

8.3.3 用 ocrmypdf 给扫描件添加文字层

# 安装
pip install ocrmypdf

# 基本用法:给扫描 PDF 加 OCR 文字层
ocrmypdf input_scan.pdf output_searchable.pdf

# 指定语言(中文简体 + 英文)
ocrmypdf -l chi_sim+eng input.pdf output.pdf

# 跳过已有文字的页面(混合文档)
ocrmypdf --skip-text input.pdf output.pdf

# 生成 PDF/A 存档格式
ocrmypdf --output-type pdfa input.pdf output.pdf

# 自动校正倾斜扫描
ocrmypdf --deskew --clean input.pdf output.pdf

ocrmypdf 内部调用 Tesseract 做 OCR,然后用 pikepdf/reportlab 在 PDF 中精确放置透明文字层。它是目前开源 OCR PDF 方案中最成熟的。


8.4 图片 PDF 的压缩策略

8.4.1 扫描件为什么那么大

一张 300 DPI 的 A4 灰度扫描件:

显然不能这样存储。不同的压缩方案:

压缩方式 压缩率(灰度文字页) 适用场景
JPEG (DCTDecode) ~10:1 → 800KB/页 照片、彩色文档
JPEG 2000 (JPXDecode) ~15:1 → 550KB/页 高质量要求
JBIG2 ~50:1 → 160KB/页 黑白文字扫描件
CCITT Group 4 ~20:1 → 400KB/页 传真、简单黑白
Deflate + PNG pred. ~5:1 → 1.6MB/页 无损要求

8.4.2 JBIG2:极致压缩的代价

JBIG2 是专为黑白文档设计的压缩算法,其原理极其聪明:

  1. 符号字典:扫描页面中所有字符形状,把相似的归为一类。比如页面中出现了 50 个 "e",形状略有不同(扫描噪声),但 JBIG2 只存储一个"代表 e"的模板。
  2. 替换编码:把页面中每个字符替换为"字典中的第 N 个模板,放在坐标 (x,y)"。
  3. 残差编码:如果某个字符和模板差异较大,可以存储差异(有损 JBIG2 会跳过这步)。

有损 JBIG2 的压缩率惊人(50:1 甚至更高),但有一个著名的问题:它可能改变文字内容

2013 年发现的案例:某 Xerox 复印机使用有损 JBIG2 压缩扫描件,把文档中的 "6" 替换成了 "8"(因为这两个字符的扫描图像被判定为"足够相似"而归入同一类)。在建筑蓝图的尺寸标注中,这种错误可能导致严重后果。

8.4.3 混合分层压缩(MRC)

高级扫描仪和 OCR 软件使用 **MRC(Mixed Raster Content)**分层压缩——和 DjVu 的思想相同:

前景层(文字/线条):JBIG2 或 CCITT,1-bit,高分辨率
背景层(图片/底色):JPEG,低分辨率
蒙版层:决定哪些像素来自前景、哪些来自背景

一页彩色杂志用 MRC 压缩后:文字锐利(来自高分辨率前景层),照片平滑(来自低分辨率背景层),总文件大小只有 JPEG 的 1/3。


8.5 扫描质量对 OCR 的影响

8.5.1 DPI 与识别率

扫描 DPI 英文识别率 中文识别率 文件大小(灰度 A4)
100 ~85% ~70% ~120 KB (JPEG)
150 ~92% ~82% ~200 KB
200 ~96% ~90% ~350 KB
300 ~99% ~96% ~800 KB
600 ~99.5% ~98% ~3 MB

300 DPI 是 OCR 的甜蜜点——再高收益递减,但文件急剧膨胀。

8.5.2 常见的扫描问题及影响

问题 对 OCR 的影响 解决方案
倾斜 行检测失败 自动 deskew(倾斜校正)
噪点(椒盐噪声) 误识别小点为标点 去噪滤波
透页(背面文字透过来) 干扰前景文字识别 二值化阈值调整
曲面失真(书脊处弯曲) 字符变形 dewarping(去翘曲)
低对比度(复印件的复印件) 整体识别率下降 对比度增强
手写标注覆盖印刷文字 无法区分两层 较难处理

8.5.3 一个有趣的数据

Google Books 项目扫描了超过 4000 万本书。他们使用的扫描设备分辨率为 400-600 DPI,配合自研的 OCR 引擎。据估计,对于质量好的印刷书籍,他们的字符级准确率超过 99.9%——这意味着每 1000 个字还是可能有 1 个错误。

一本 10 万字的书,可能有 100 个 OCR 错误散布其中。对于人类读者来说几乎不影响理解(大脑会自动纠错),但对于精确性要求高的场景(法律文献、学术引用),这些错误可能是不可接受的。


8.6 判断一份 PDF 是否有文字层

8.6.1 快速判断方法

import fitz  # PyMuPDF

doc = fitz.open("unknown.pdf")
page = doc[0]

# 提取文字
text = page.get_text()

if len(text.strip()) < 10:
    print("这一页可能是纯图片(无文字层)")
else:
    print(f"有文字层,提取到 {len(text)} 个字符")
    
# 检查页面中的图片
images = page.get_images()
print(f"页面包含 {len(images)} 张图片")

# 如果有 1 张占满页面的大图 + 几乎无文字 → 纯扫描件
# 如果有 1 张大图 + 有文字 → OCR 增强 PDF
# 如果无大图 + 有文字 → 原生文字 PDF

8.6.2 命令行工具

# 用 pdftotext 提取文字,看输出是否为空
pdftotext document.pdf - | head -20

# 用 pdfimages 列出所有嵌入图片
pdfimages -list document.pdf

# 输出示例(纯扫描件):
# page  num  type  width  height  color  comp  bpc  ...
#    1    0  image  2480   3508   gray     1    8  ...

如果每一页都只有一张和页面等大的图片,且 pdftotext 输出为空或只有垃圾字符——那就是纯扫描件。