引言
OCR(Optical Character Recognition,光学字符识别)把图像中的文字转成可编辑文本。它看起来是个老问题,但真正难的不是单字识别,而是把「检测、矫正、识别、版面、结构化」串成一条在真实文档上稳定的流水线。票据、合同、证件、工业铭牌的版式千差万别,扫描质量、光照、倾斜、印章遮挡都会让准确率断崖式下跌。
工程上最常见的误区是把 OCR 当成纯粹的模型问题:以为换个更大的识别网络就能提升效果,实际上多数失败来自文本检测漏检(小字、密集行)、预处理没做倾斜矫正、以及缺少领域后处理(字典、正则、校验位)。另一个误区是忽略「结构化」——业务要的往往不是一串文本,而是「发票号、金额、开票日期」这样的键值对。
本文按「任务拆解 → 检测 → 识别 → 框架选型 → 预处理 → 版面与表格 → 结构化抽取 → 评估与纠错」的顺序讲,重点给出可直接运行的代码与真实取舍。检测与识别的基础概念可先看 目标检测与 YOLO 系列工程落地 ,图像预处理手段见 OpenCV 图像基础与预处理 。
目录
- OCR 任务拆解与链路
- 文本检测:DBNet 与可微二值化
- 文本识别:CRNN 与 CTC
- 端到端 OCR 框架选型
- 预处理:倾斜矫正与低质量增强
- 代码:PaddleOCR 端到端识别
- 代码:CTC 解码与编辑距离评估
- 版面分析与表格识别
- 文档结构化抽取
- 中文与多语言识别难点
- 评估指标与后处理纠错
- 部署与性能优化
1. OCR 任务拆解与链路
工业级 OCR 是一条多级流水线,每级都可能成为瓶颈:
| 阶段 | 输入 | 输出 | 典型方法 |
|---|---|---|---|
| 预处理 | 原始图像 | 矫正、增强后的图 | 去噪、二值化、透视矫正 |
| 文本检测 | 图像 | 若干文本行四边形 | DBNet、PSENet、CRAFT |
| 方向分类 | 文本行裁剪 | 0/90/180/270 | 轻量分类网络 |
| 文本识别 | 文本行裁剪 | 字符串 | CRNN+CTC、SVTR、TrOCR |
| 版面分析 | 整页图像 | 区域类型与顺序 | LayoutLM、PP-Structure |
| 结构化 | 文本行 + 版面 | 键值对、表格 | 规则 + 模型 |
把链路拆开的价值在于定位问题:如果最终字段错误,先看是检测框没框住、识别错字、还是版面顺序错。不做拆解就只能盲目换模型。
2. 文本检测:DBNet 与可微二值化
文本检测与通用目标检测的区别在于目标是「细长不规则区域」,且相邻字符必须合并成一个文本行。
DBNet(Differentiable Binarization)的思路是:网络先预测一个概率图 P,再用一个自适应阈值图 T,通过近似的阶跃函数得到二值图 B。因为二值化可微,整个流程可以端到端训练。它的后处理简单:对二值图取连通域,再用 Vatti 算法把轮廓膨胀回文本框。DBNet 在 ICDAR2015 上约 87% 的 Hmean,速度也很快,是工业首选。
其他方案:
- CRAFT:预测字符区域与字符间连接,擅长处理任意形状与密集文本。
- PSENet:用渐进式尺度扩张分离紧邻文本实例。
- EAST:早期方案,直接回归框的几何参数,对长文本不如 DBNet 稳。
工程上最常调的两个参数是 det_db_thresh(概率图阈值,默认 0.3)与 det_db_box_thresh(框置信度阈值,默认 0.6)。低质量图要下调阈值防漏检,但会引入噪声框。
3. 文本识别:CRNN 与 CTC
CRNN(Convolutional Recurrent Neural Network)是识别分支的经典结构,三段式:
- 卷积层提取特征,得到高度为 1 的特征图(如 H/32 × W/8)。
- 循环层(BiLSTM)沿宽度方向建模字符间的序列依赖。
- 转录层把序列映射成字符串。
难点在于「不定长对齐」:图像宽度与字符数不对应,无法逐帧标注。CTC(Connectionist Temporal Classification)用「空白符号 + 多对一折叠」解决:允许输出重复字符与空白,最后折叠重复并去掉空白。例如序列 -a-a-b-b- 折叠为 ab。
CTC 的损失对每一帧的输出做边缘化求和,训练时不需要字符级位置标注,这是它能在工业上大规模使用的原因。缺点是假设「字符间条件独立」,对语言模型约束弱,中文长句容易出错。改进方向有 Attention 解码(如 TrOCR)、以及识别后接语言模型纠错。
import torch
import torch.nn as nn
# CTC 损失:log_probs 形状 (T, N, C),targets 是拼接后的标签
ctc = nn.CTCLoss(blank=0, reduction="mean", zero_infinity=True)
T, N, C = 26, 4, 38 # 时间步、批大小、字符集大小(含 blank)
log_probs = torch.log_softmax(torch.randn(T, N, C), dim=2)
targets = torch.randint(1, C, (N, 8), dtype=torch.long) # 每样本 8 个字符
input_lengths = torch.full((N,), T, dtype=torch.long)
target_lengths = torch.full((N,), 8, dtype=torch.long)
loss = ctc(log_probs, targets, input_lengths, target_lengths)
print("ctc loss:", float(loss))
注意 CTCLoss 的输入必须是 log_softmax 之后的值,且时间维在最前。忘记 log_softmax 是最常见的报错来源。
4. 端到端 OCR 框架选型
自己从零实现检测加识别不现实,工程上直接用成熟框架。
| 框架 | 检测 | 识别 | 中文支持 | 特点 |
|---|---|---|---|---|
| PaddleOCR | DBNet | CRNN/SVTR | 极好 | 中文场景事实标准,模型全 |
| Tesseract | 内置 | LSTM | 一般 | 老牌,无 GPU 也能跑,版面弱 |
| EasyOCR | CRAFT | CRNN | 好 | API 简单,80+ 语言 |
| MMOCR | 多种 | 多种 | 好 | OpenMMLab 生态,可换模块 |
| TrOCR | 无 | Transformer | 一般 | 印刷体英文强,中文需微调 |
| docTR | DBNet | CRNN/ViT | 一般 | 文档场景友好 |
选型建议:中文与票据场景优先 PaddleOCR;需要换模块做研究用 MMOCR;只要一个简单 API 用 EasyOCR;纯英文印刷体且要求离线轻量可用 Tesseract。
5. 预处理:倾斜矫正与低质量增强
预处理往往比换模型更有效。常见手段:
- 灰度化与去噪:中值滤波去椒盐噪声,双边滤波保边去噪。
- 二值化:全局 Otsu 适合光照均匀,自适应阈值适合光照不均。
- 倾斜矫正:用霍夫变换估计文本行倾角,或用最小外接矩形角度做仿射矫正。
- 透视矫正:检测文档四角点,用
getPerspectiveTransform展平。 - 超分与去模糊:低分辨率票据可先用 Real-ESRGAN 类模型放大再识别。
- 对比度增强:CLAHE 对低对比度扫描件提升明显。
import cv2
import numpy as np
def deskew(gray):
# 用 Otsu 反色后找非零点的最小外接矩形角度做矫正
thr = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
coords = np.column_stack(np.where(thr > 0))
angle = cv2.minAreaRect(coords)[-1]
if angle < -45:
angle = 90 + angle
h, w = gray.shape[:2]
M = cv2.getRotationMatrix2D((w / 2, h / 2), angle, 1.0)
return cv2.warpAffine(gray, M, (w, h), flags=cv2.INTER_CUBIC,
borderMode=cv2.BORDER_REPLICATE)
gray = cv2.cvtColor(cv2.imread("receipt.jpg"), cv2.COLOR_BGR2GRAY)
print("deskewed:", deskew(gray).shape)
注意 minAreaRect 返回的角度语义在不同 OpenCV 版本间有差异,务必用可视化验证,不要盲信。
6. 代码:PaddleOCR 端到端识别
PaddleOCR 把检测、方向分类、识别封装成一次调用。
from paddleocr import PaddleOCR
# use_angle_cls 开启方向分类,lang 指定语言模型
ocr = PaddleOCR(use_angle_cls=True, lang="ch", show_log=False)
result = ocr.ocr("invoice.jpg", cls=True)
for line in result[0]:
box, (text, score) = line # box 是四个角点,text 是识别串
print(f"{score:.3f} {text}")
# 版面分析 + 表格识别(PP-Structure)
from paddleocr import PPStructure
engine = PPStructure(show_log=False, layout=True, table=True)
for region in engine("page.png"):
print(region["type"], region.get("res", "")[:40])
cls=True 会多跑一次方向分类,慢一点但能救回倒置的文本行。版面分析返回的 region["type"] 常见取值为 text、title、table、figure,表格会额外返回 HTML 结构。
7. 代码:CTC 解码与编辑距离评估
训练之外,评估环节需要贪心解码与编辑距离。下面手写一遍,便于理解框架内部行为。
import numpy as np
def ctc_greedy_decode(logits, blank=0):
# logits: (T, C),返回去掉 blank 并折叠重复后的索引序列
idx = logits.argmax(axis=1)
out, prev = [], None
for i in idx:
if i != prev and i != blank:
out.append(int(i))
prev = i
return out
def edit_distance(a, b):
# 标准 Levenshtein 距离,用于计算字符错误率 CER
dp = list(range(len(b) + 1))
for i, ca in enumerate(a, 1):
new = [i]
for j, cb in enumerate(b, 1):
new.append(min(dp[j] + 1, new[j - 1] + 1, dp[j - 1] + (ca != cb)))
dp = new
return dp[-1]
logits = np.random.randn(20, 10)
pred = ctc_greedy_decode(logits)
print("decoded:", pred)
print("CER:", edit_distance([1, 2, 3], [1, 2, 4, 5]) / 4) # 0.5
贪心解码实现简单但非最优,同一输入用 beam search 常能少错一两个字;线上对延迟敏感时贪心仍是主流。
8. 版面分析与表格识别
版面分析回答「这一页有哪些区块、顺序如何」,是文档理解的关键一环。
- 区域检测:把页面切成标题、正文、表格、图片、页眉页脚等区域。
- 阅读顺序:中文与英文的阅读顺序不同,多栏排版需要排序算法或模型预测。
- 表格识别:先检测表格线或单元格,再逐格 OCR,最后按行列还原成 HTML 或二维数组。
- 无线表格:没有框线的表格靠单元格坐标聚类还原,难度显著更高。
常用模型有 LayoutLMv3、PP-Structure、以及基于 DETR 的版面检测器。评估指标常用 mAP 与区域级的 F1。
表格还原的坑在于「合并单元格」:跨行跨列会让行列索引错乱,工程上要显式建模 rowspan/colspan。
9. 文档结构化抽取
业务要的是键值对,不是文本行。抽取分两条路线:
- 规则路线:用关键词定位字段(如「发票号码」右侧最近文本),配正则与校验位。成本低、可解释、适合版式固定的票据。
- 模型路线:用 LayoutLM 系列做多模态序列标注,把文本、位置、视觉特征一起输入 Transformer,直接输出字段标签。适合版式多变、字段语义复杂的场景。
import re
PATTERNS = {
"invoice_no": r"发票号码[::]?\s*([0-9]{8,20})",
"amount": r"(?:价税合计|金额)[^\d]{0,6}([0-9]+\.[0-9]{2})",
"date": r"(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日",
}
def extract(text):
out = {}
for key, pat in PATTERNS.items():
m = re.search(pat, text)
if m:
out[key] = m.group(0) if key == "date" else m.group(1)
return out
print(extract("发票号码:12345678 价税合计 1200.00 2026年10月07日"))
规则与模型通常混用:模型做初抽,规则做校验(如金额合计是否等于明细之和),不一致时转人工。
10. 中文与多语言识别难点
中文 OCR 比英文难在四点:
- 字符集大:常用汉字 6000+,加上符号与生僻字,分类头远大于英文的 26 个字母。
- 字形相近:己/已/巳、日/曰、未/末,低分辨率下极易混淆。
- 无词边界:英文可借空格分词,中文必须靠语言模型消歧。
- 混排:中英数字标点混排,需要统一字符集与全半角归一化。
多语言场景还要处理从右到左(阿拉伯语)、上下结构(泰语)、以及变音符(越南语)。工程手段是分语言训练识别模型,或用共享字符集的统一模型;后处理上做全角转半角、繁简统一、以及领域词典约束。
手写体是另一个量级:连笔、笔画粘连、书写风格差异大,通常需要专门的标注数据与更强的序列模型,通用印刷体模型直接迁移效果很差。
11. 评估指标与后处理纠错
OCR 的指标必须分开看检测与识别:
| 指标 | 含义 | 适用 |
|---|---|---|
| Precision / Recall / Hmean | 检测框的准确与召回 | 文本检测 |
| 字符准确率 | 正确字符数 / 总字符数 | 识别 |
| 词准确率 | 整串完全正确的比例 | 识别,更严格 |
| CER | 编辑距离 / 真值长度 | 识别,越低越好 |
| 字段准确率 | 结构化字段全对的比例 | 业务最终指标 |
注意「字符准确率 99%」听起来很高,但一张票据 200 个字符,意味着平均两张就有一个错字,字段级准确率可能只有 80%。业务指标要用字段准确率而非字符准确率。
后处理纠错三招:
- 词典约束:识别结果与领域词表做模糊匹配,纠正形近字。
- 规则校验:身份证校验位、金额格式、日期合法性。
- 语言模型重排:用 n-gram 或小型 LLM 对 beam 候选重排,提升长句正确率。
def cer(pred, truth):
if not truth:
return 0.0
return edit_distance(pred, truth) / len(truth)
print("CER:", round(cer("发票号玛", "发票号码"), 4)) # 0.25
12. 部署与性能优化
OCR 服务化的瓶颈常在检测阶段(高分辨率整图)与预处理(CPU 串行解码)。
- 检测降采样:整图缩放到长边 960 到 1280 通常够用,再大收益递减。
- 批处理:识别阶段把多个文本行拼成一个批,吞吐提升明显。
- 模型选择:移动端用 PP-OCRv4 mobile 系列,参数量几 M,CPU 也能跑。
- 推理引擎:导出 ONNX 后用 ONNX Runtime 或 TensorRT,FP16 通常有 1.5 到 2 倍加速。
- 缓存:同一文档重复提交时按哈希缓存结果,省掉重复推理。
- 异步流水线:检测与识别分到不同 worker,请求在阶段间流动。
性能与精度的平衡点要靠压测确定,方法与 视觉服务化与推理加速 中的动态批处理与延迟分析一致。
权衡取舍
| 维度 | 倾向 | 代价 |
|---|---|---|
| 通用框架与自训练 | 先用通用框架,难例再微调 | 微调需标注数据 |
| 规则抽取与模型抽取 | 固定版式用规则,多变版式用模型 | 模型需 GPU 与标注 |
| 检测精度与速度 | 提高分辨率提升小字召回 | 延迟与显存上升 |
| 贪心解码与 beam search | 延迟敏感用贪心,精度优先用 beam | beam 慢数倍 |
| 端到端与多级流水 | 多级易定位问题,端到端省工程 | 端到端难调试 |
| 字典约束与开放识别 | 领域字典提升准确率 | 生僻字被误纠 |
几条实用原则:
- 先测「检测召回率」,检测漏了的字,识别再强也救不回。
- 字段准确率才是业务指标,不要用字符准确率汇报。
- 领域词典与校验规则的成本远低于重训模型,优先做。
- 低质量图先做预处理,往往比换大模型收益更大。
常见坑清单
- 预处理与训练不一致:训练用灰度二值图,线上直接送彩色原图,掉点严重。
- 忘记方向分类:倒置或竖排文本行识别成乱码,
cls=True能救回大部分。 - 检测阈值过高:低对比度票据上的浅色小字直接漏检,需下调
det_db_thresh。 - 框未做外扩:紧贴文字的检测框会切掉笔画边缘,识别出错,需按比例外扩。
- 中文全半角混用:
123与123是两个字符,必须归一化后再匹配字段。 - 表格合并单元格不建模:行列索引错位,抽取的字段张冠李戴。
- 用字符准确率汇报:掩盖字段级错误,业务验收时被打回。
- 忽略阅读顺序:多栏文档按检测顺序拼接,语义完全错乱。
- 缺校验位验证:身份证与银行卡号不做校验,错一位也当成功。
- 训练数据与线上字体不匹配:新字体上线后准确率骤降,需持续回流难例。
- 忘记归一化:识别模型输入必须做与训练一致的缩放与填充。
小结
OCR 工程的主线是:预处理矫正 → DBNet 检测 → 方向分类 → CRNN 加 CTC 识别 → 版面分析 → 规则与模型结合做结构化 → 字段级评估与后处理纠错。记住三个判断点:检测召回决定上限、预处理性价比最高、字段准确率才是业务指标。中文场景优先用 PaddleOCR 这类成熟框架,把精力投在预处理、领域词典与校验规则上。
当版式高度多变、字段语义复杂时,再引入 LayoutLM 系列做多模态序列标注;当文档规模巨大且重复率高时,优先做缓存与增量处理。OCR 的上限往往由数据质量与业务规则决定,而不是网络结构。
延伸阅读
- OpenCV 图像基础与预处理 — 倾斜矫正与二值化的实现细节
- 目标检测与 YOLO 系列工程落地 — 文本检测的检测范式基础
- 数据标注与数据集工程 — 文本行标注格式与质量
- 模型训练与迁移学习 — 领域字体微调的策略
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。