简介:面向希望快速了解文档OCR识别实现的开发者与文档处理学习者,这份资源围绕图片文字转机器编码文字的需求,提供可运行的示例脚本与配套样例图,适合作为OCR入门实践和流程验证的参考。压缩包共3个文件,包含2个Python脚本和1张JPG示例图片,体积约2.03MB;脚本可用于演示与测试识别流程,图片则便于直接对照验证识别效果。已有192人学习或下载,说明其对初学者和需要快速复现OCR小实验的读者具有一定参考价值。读者可借助脚本结构理解OCR调用方式、识别结果输出与简单测试思路,并结合示例图片观察不同文档图像的识别表现,为后续接入开源工具或在线OCR服务、处理实际文档场景打下基础。
1. 扫描件堆成山:OCR 落地真正卡住的是版面而不是模型
前段时间帮某实验室整理一批纸质档案,扫描仪吐出来七百多页 PDF,肉眼能读,但关键词搜不到、表格复制不出来、页码顺序还是乱的。这正是 OCR 要解决的事:把图片里的文字变成可编辑、可检索的结构化数据。模型本身早就不是门槛,门槛在版面还原、表格结构、置信度筛选这些工程细节上。这篇文章围绕一套能直接跑的 OCR 流水线展开,从开源引擎选型、图像预处理、检测识别串联,到批量验收和排错,适合手里有扫描件、合同、旧书要数字化的开发者,也适合正在给业务系统加一个文档识别模块、做软件或插件的同学。照着走完,至少能自己搭出一条稳定出结果的链路,而不是每次换一批图就翻车。
2. 选型先立住:开源引擎与云端接口的能力边界在哪
2.1 四类主流方案的横向对比
OCR 这个词被用得很大,但落到工程上其实分两条路线:本地开源引擎和云端识别接口。前者把模型和推理都放在自己机器上,后者把图像传出去换结构化结果。两者没有绝对优劣,取决于你对数据出域、调用成本、维护人力的容忍度。先把常见四类方案摆在一张表里,看得更清楚。
| 方案 | 中文印刷体表现 | 部署方式 | 版面/表格能力 | 冷启动 | 典型适用场景 |
|---|---|---|---|---|---|
| Tesseract | 中规中矩,需调参或自训练 | 本地 CLI / 库 | 弱,需自己切版 | 快 | 英文文档、老旧系统内嵌 |
| PaddleOCR | 较强,中文场景开箱可用 | 本地 Python / 服务化 | 中,配合版面分析可用 | 中 | 中文档案、票据、合同 |
| EasyOCR | 尚可,多语种友好 | 本地 Python | 弱 | 首次要下模型 | 快速原型、多语言混排 |
| 云端 OCR 接口 | 强,常带字段级结构化 | HTTP 调用 | 强 | 依赖网络 | 发票字段抽取、低维护场景 |
表格里的“强/弱”是相对感受,不是精确指标。我一般会先用 PaddleOCR 打底,因为它的检测、方向分类、识别是三段式流水线,中文模型自带字典,mobile 版本体积小,能离线跑;Tesseract 留着做英文纯文本兜底,命令行一行就能出结果,适合塞进脚本;EasyOCR 只在需要多语种混排时用来快速验证;云端接口则留给“字段抽取要求高、又不想维护模型”的项目。
这里要提醒一句:很多教程只对比“识别准确率”,但真实项目里更痛的是版面。一个扫描件里同时有大标题、两栏正文、页脚、表格,模型逐行识别能把字认对,顺序却全乱了。所以选型时除了看识别模型,还要看有没有版面分析能力,或者愿不愿意自己写切块逻辑。
2.2 为什么用 PaddleOCR 打底、Tesseract 兜底
选 PaddleOCR 的核心理由不是“准确率最高”,而是可控。它的推理参数暴露得足够细:检测框阈值、框外扩比例、识别置信度过滤、方向分类开关都能在实例化时传进去。这意味着同一套代码,面对发票、书籍、屏幕截图可以调不同参数,而不用换框架。另外中文识别模型自带常用汉字字典,生僻字和专业术语可以通过自定义字典补,不需要从头训练。
Tesseract 的价值在于“极简兜底”。它不需要深度学习运行时,装完就能用,英文识别在干净扫描件上表现稳定。我的习惯是:流水线里先用 PaddleOCR 出主结果,遇到置信度普遍偏低的页面,切到 Tesseract 跑一遍做交叉验证,两边差异大的行人工复核。这样比单一引擎硬扛要靠谱。
EasyOCR 和云端接口不是不能用,而是要认清代价。EasyOCR 首次运行会下载模型,内网环境容易卡住;云端接口则要处理网络抖动、超时重试、费用统计,以及最关键的——数据能不能出域。做内部档案数字化的项目,这一条往往直接否决云端方案。
2.3 环境安装与最小可跑通验证
先建独立虚拟环境,避免和系统里的其他深度学习依赖打架。安装命令不锁死小版本,但要求最低版本,方便复现又不至于装不上。
# 建虚拟环境,python 3.8 以上即可 python -m venv ocr-env source ocr-env/bin/activate # Windows 用 ocr-env\Scripts\activate # 安装推理框架与 OCR 库 pip install "paddlepaddle>=2.5" pip install paddleocr opencv-python pillow pytesseract安装完先跑最小验证,别急着上批量脚本。下面这段代码做三件事:初始化引擎、识别单图、按置信度打印结果。注意use_angle_cls控制是否做 0/180 度方向判断,扫描件倒置很常见,建议开着。
from paddleocr import PaddleOCR # use_angle_cls 开启方向分类;lang 指定中文模型 ocr = PaddleOCR(use_angle_cls=True, lang='ch', show_log=False) # cls=True 表示对每个检测框做方向分类 result = ocr.ocr('sample.png', cls=True) # result[0] 是列表,每项为 [四点坐标, (文本, 置信度)] for line in result[0]: box, (text, score) = line print(f'{score:.3f}\t{text}')逻辑说明:ocr.ocr()返回的是按页组织的列表,单图时取result[0]。每个元素里四点坐标是检测框,后面跟着识别文本和置信度。参数说明:lang='ch'会加载中文检测与识别模型,如果文档以英文为主可以换成en,速度更快;show_log=False只是关掉框架日志,排错时可以打开看模型加载路径。第一次运行会自动下载模型到用户目录,内网机器需要提前把模型文件拷进去。
Tesseract 的兜底调用更简单,但要指定语言包和版面模式:
import pytesseract from PIL import Image # lang 用 chi_sim+eng 支持中英混排;psm 6 表示按统一文本块处理 text = pytesseract.image_to_string( Image.open('sample.png'), lang='chi_sim+eng', config='--psm 6' ) print(text)--psm是页面分割模式,6 适合单栏连续文本,3 适合自动分页,11 适合稀疏文字。中文语言包需要单独安装,装完用tesseract --list-langs确认chi_sim在列表里,不在的话识别会直接退化成英文,这是新手最常踩的第一个坑。
3. 图像预处理:把识别率从 70% 拉到 90% 的四个参数
3.1 灰度与自适应二值化的参数怎么定
扫描件的第一个问题是光照不均:书脊附近发黑,边缘过曝。直接丢给 OCR 模型,检测框会漏掉暗区文字。常规做法是先转灰度,再做自适应二值化,让每个局部区域有自己的阈值。
import cv2 import numpy as np img = cv2.imread('scan.jpg') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # blockSize 必须是奇数,越大越能适应大面积光照变化 # C 是常数项,越大二值化越激进,文字越容易变粗 binary = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blockSize=31, C=15 ) cv2.imwrite('binary.png', binary)逻辑说明:高斯加权的自适应阈值比全局阈值更适合书本扫描件。参数说明:blockSize=31意味着每 31×31 的邻域算一个阈值,页面分辨率在 2000 像素宽左右时这个值比较稳;如果扫描分辨率很高,比如 4000 像素宽,blockSize要相应加大到 51 或 61,否则邻域太小会把纸纹也当成文字。C=15是我常用的起点,字迹浅的档案可以降到 8 到 10,噪点多就往上加,但超过 25 容易把细笔画吃掉。
这里有个反直觉的点:二值化不一定总是让识别变好。对于彩色背景、印章覆盖、手写批注的文档,强行二值化会把印章和文字糊成一团,反而降低召回。我的做法是保留原图跑一遍、二值图跑一遍,按置信度均值决定用哪份结果。这一步多花的时间,比后面人工纠错便宜得多。
3.2 倾斜校正与去噪:先摆正再识别
扫描时纸张没放平,整页倾斜两三度,检测框就会互相重叠,识别结果串行。校正思路是用前景像素的最小外接矩形估角度,再仿射旋转。
# 取前景(黑色像素)坐标,估整体倾斜角 coords = np.column_stack(np.where(binary < 128)) angle = cv2.minAreaRect(coords)[-1] # minAreaRect 返回的角度在 [-90, 0),换算成旋转角 if angle < -45: angle = -(90 + angle) else: angle = -angle (h, w) = img.shape[:2] M = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) rotated = cv2.warpAffine( img, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE )逻辑说明:minAreaRect对整页文字块给出近似主轴角度,适合规则排版;如果页面里表格线很多,外接矩形会被表格带偏,这时改用霍夫直线检测取主方向更稳。参数说明:INTER_CUBIC插值质量高但慢,批量处理可以换INTER_LINEAR;BORDER_REPLICATE用边缘像素填充旋转后的空白,避免出现黑边干扰检测。
去噪放在校正之后。椒盐噪声用中值滤波,细小断笔用形态学闭运算补:
# 中值滤波去孤立噪点,核大小 3 足够,太大会糊掉笔画 denoised = cv2.medianBlur(rotated, 3) # 闭运算连接断裂的笔画,kernel 用 2x2 避免过度膨胀 kernel = np.ones((2, 2), np.uint8) closed = cv2.morphologyEx(denoised, cv2.MORPH_CLOSE, kernel)顺序很重要:先校正再滤波,否则旋转插值会把噪点扩散成一片。这一步在低质量复印件上效果最明显,我见过识别率从六成出头拉到九成的情况,靠的就是摆正加去噪,模型一行没改。
3.3 版面分析:先切块再识别,顺序才不会乱
多栏文档、带页眉页脚的合同,如果整页丢给识别模型,输出顺序基本靠检测框的坐标排序,经常出现左栏和右栏交替串行。解决办法是先做版面切块,按阅读顺序拼结果。
规则版面可以用水平投影找行间空白,实现简单、速度快:
# 统计每行前景像素数,低于阈值的行视为行间空白 row_sum = np.sum(binary < 128, axis=1) row_thresh = binary.shape[1] * 0.01 text_rows = np.where(row_sum > row_thresh)[0] # 把连续行号合并成文本行区间 blocks, start = [], text_rows[0] for i in range(1, len(text_rows)): if text_rows[i] - text_rows[i - 1] > 1: blocks.append((start, text_rows[i - 1])) start = text_rows[i] blocks.append((start, text_rows[-1]))逻辑说明:row_thresh取页面宽度的百分之一是经验值,太小会把行内空隙也切开,太大则整页并成一块。参数说明:这段只处理单栏;两栏及以上要先做垂直投影找栏间空白,按栏切完再在每栏内做水平投影。对于表格密集的页面,投影法会失效,这时候换成轮廓检测或直接用版面分析模型,把表格区域单独裁出来走表格识别。
复杂版面我一般不再手写规则,直接上版面分析模型输出区域框,再按框的坐标排序识别。多引入一个模型会增加启动时间和内存,但对合同、年报这类文档,省下的人工校对时间远超这点开销。
4. 检测与识别串联:DBNet 阈值、字典与后处理的三段式
4.1 检测框阈值:三个参数决定漏检还是误检
PaddleOCR 的检测阶段用的是 DBNet 系思路,输出概率图后做二值化得到文字区域。三个参数最影响结果:det_db_thresh控制概率图二值化,det_db_box_thresh控制框得分过滤,det_db_unclip_ratio控制框外扩。调参前先明白它们各自管什么。
from paddleocr import PaddleOCR ocr = PaddleOCR( lang='ch', use_angle_cls=True, det_db_thresh=0.3, # 概率图二值化阈值,调低召回更多候选 det_db_box_thresh=0.5, # 框得分低于此值丢弃,调高更严格 det_db_unclip_ratio=1.6, # 框向外扩张比例,小字密集时调大 drop_score=0.5, # 识别置信度低于此值不输出 show_log=False )逻辑说明:检测先得到候选框,再按得分过滤,最后外扩一点把文字边缘包进去。参数说明:det_db_thresh默认 0.3,遇到浅色印刷或低对比度扫描件可以降到 0.2,但会引入更多噪声框;det_db_box_thresh默认 0.6 左右,票据类小字多,我通常降到 0.4 到 0.5 保召回;det_db_unclip_ratio默认 1.5,宋体小字容易被切掉边,调到 1.8 到 2.0 更稳,但相邻行距很窄时会把两行框到一起。drop_score是识别阶段的过滤线,宁可低一点再人工复核,也不要一开始就设 0.8 把正确结果扔掉。
调参的正确姿势是拿十张有代表性的图做小样本对比,记录漏检行数和误检行数,而不是凭感觉改。我见过有人把det_db_thresh一路调到 0.1,结果整页都是碎框,识别阶段反而更慢更差。
4.2 识别模型与字典:中文场景真正的坑
识别阶段是检测框送进 CRNN 或 SVTR 类模型,输出字符序列。中文的难点在字典:模型只能输出字典里有的字。默认中文模型带常用汉字表,遇到人名生僻字、化学式、专业型号,会出现“识别成形近字”或直接吞字。
处理方式有三层。第一层是换模型,lang='ch'用通用中文模型,lang='chinese_cht'针对繁体,英文为主的混排可以试en。第二层是自定义字典,把业务高频词加进字典文件,推理时指定。第三层才是微调训练,成本最高,只有在通用模型系统性出错时才值得做。
# 指定自定义字典,每行一个字符或词 ocr = PaddleOCR( lang='ch', rec_char_dict_path='./dict/business_dict.txt', use_angle_cls=True, drop_score=0.3 )逻辑说明:字典文件决定输出字符空间,模型结构不变时替换字典要保证字典与训练时一致,否则输出会错位。参数说明:自建字典适合“补字”,不适合“改模型认知”;如果某个词总是被识别成形近词,说明模型特征层面就没学好,加字典只能保证这个字能输出,不能保证选对。这种情况要靠后处理纠错兜底。
另一个常被忽略的点是方向分类。use_angle_cls=True会对每个框判断是否 180 度倒置,代价是额外一次推理。对于整页方向一致的扫描件,可以在预处理阶段统一旋转,然后关掉这个开关,批量处理能省不少时间。
4.3 后处理:正则清洗与字段结构化
模型输出的是带空格的原始文本,直接入库很难用。后处理做三件事:去噪、纠错、结构化。去噪是清理识别插入的多余空格和不可见字符,纠错是按业务规则修形近字,结构化是把整段文本映射成字段。
import re def normalize(text: str) -> str: # 去掉识别产生的空格和制表符 text = re.sub(r'\s+', '', text) # 全角转半角,统一标点 text = text.replace(':', ':').replace(',', ',') # 去掉除中文、字母、数字和常用符号外的字符 text = re.sub(r'[^\u4e00-\u9fa5A-Za-z0-9\-./:%]', '', text) return text def fix_common(text: str) -> str: # 按业务词典修正形近字,顺序从长词到短词 rules = {'己方': '甲方', '合问': '合同', 'O元': '0元'} for wrong, right in rules.items(): text = text.replace(wrong, right) return text逻辑说明:先清洗再纠错,避免空格干扰规则匹配。参数说明:正则里的字符白名单要按业务收紧,比如金额场景保留小数点和负号,地址场景保留中文数字和“号”“室”。纠错规则表不要写太长,超过几十条就该考虑用编辑距离或语言模型打分,手写规则维护不动。
字段结构化用正则从清洗后的文本里抽:
| 字段 | 正则示例 | 说明 |
|---|---|---|
| 合同编号 | [A-Z]{2,4}-?\d{6,12} | 前缀字母加数字,允许连字符 |
| 金额 | \d+(\.\d{1,2})?元 | 限定“元”结尾,减少误匹配 |
| 日期 | \d{4}[-/年]\d{1,2}[-/月]\d{1,2} | 兼容三种分隔符 |
| 统一编码 | [0-9A-Z]{18} | 长度固定,校验位另算 |
正则抽完之后一定要过一遍校验位或业务规则,比如金额上限、日期合法性。OCR 输出里0和O、1和l、5和S的混淆非常常见,纯正则会把SOO元抽成金额。我的习惯是在字段抽取后加一层数值合法性检查,不合法就回退到原始文本让人工看,而不是硬转。
5. 避坑与排查:五条血泪经验与并发资源边界
5.1 五条高频翻车记录
现象一:中文识别结果全是拼音或乱码。原因通常是语言包没装对。Tesseract 只装了英文包,指定chi_sim会静默回退;PaddleOCR 的lang传了不支持的取值,加载的是默认模型。解决方式是先跑tesseract --list-langs确认语言包,PaddleOCR 则检查模型下载目录里是否有ch_PP-OCR相关文件夹,没有就手动下载放到指定路径。
现象二:表格识别出来行列错位,合并单元格全乱。原因是检测框把相邻单元格框成一个,或者表格线断裂导致框重叠。解决方式是表格区域单独裁出来,先做线段检测补全表格线,再按交点切单元格;实在不行换成带表格结构的识别模型,把整块表格当结构化对象处理,而不是逐行识别。
现象三:小字号内容大面积漏检。原因是原图分辨率低,检测模型下采样后文字特征消失。解决方式是把图像放大两到三倍再送检测,或者提高输入尺寸参数;同时把det_db_unclip_ratio调大。注意放大不是万能,插值放大会让边缘发虚,最好用高质量插值并配合锐化。
现象四:批量跑几百页后内存暴涨、进程被杀。原因是每张图都重新初始化引擎,或者把结果全量堆在内存里。解决方式是引擎只初始化一次,图片逐张读、逐张写盘,结果用生成器或分批落文件。GPU 环境下还要注意显存碎片,长任务定期重启 worker 比一直硬扛稳定。
现象五:同一批图,两次运行结果不一致。原因多半是预处理里用了随机性操作,或者多进程写同一个输出文件导致覆盖。解决方式是把预处理参数固定下来,随机增强只在训练时用;输出文件按图名加哈希命名,避免并发写冲突。这类问题排查起来很玄学,最有效的办法是固定随机种子并给每步中间结果落盘。
5.2 并发与资源边界:别让模型加载成为瓶颈
批量处理的性能瓶颈往往不在推理,而在反复加载模型。一个中文检测加识别模型加载要几秒到十几秒,如果每个 worker 都独立加载,八核机器光启动就耗掉一分钟。正确做法是把引擎封装成单例,或者起一个本地推理服务,worker 只负责读图和写结果。
from paddleocr import PaddleOCR from concurrent.futures import ThreadPoolExecutor # 全局只初始化一次,线程共享 _engine = PaddleOCR(lang='ch', use_angle_cls=True, show_log=False) def recognize(path): res = _engine.ocr(path, cls=True) lines = [item[1][0] for item in res[0]] if res[0] else [] return path, lines with ThreadPoolExecutor(max_workers=4) as pool: for path, lines in pool.map(recognize, image_paths): write_result(path, lines)逻辑说明:模型初始化放在模块级,线程池共享同一个引擎。参数说明:max_workers不要超过物理核数,OCR 推理是计算密集型,开太多线程只会互相抢 CPU;GPU 环境下线程数更要压低,通常 2 到 4 个就够,多了显存会爆。如果单机跑不动,把推理拆成独立服务,用队列分发任务,比在一台机器上堆进程更好扩展。
还有一个隐性成本是图像解码。大尺寸 TIFF 解码很吃内存,批量处理时限制单张图的像素上限,超过就先缩放,能避免很多“跑着跑着就崩”的问题。
6. 批量验收:CER 指标与抽样复核的一套可复用脚本
识别跑通只是开始,真正决定这套流水线能不能交付的,是有没有量化的验收标准。我一般看两个指标:字符错误率 CER 和字段准确率。CER 用编辑距离算,适合整页文本;字段准确率按业务字段逐个比对,适合票据和合同。两个都要,因为 CER 低的页面字段照样可能错位。
先准备一份人工标注的小样本,二十到五十页就够,覆盖清晰件、倾斜件、低对比度件三类。下面这段脚本算 CER 并输出最差的几页,方便定位问题:
def cer(ref: str, hyp: str) -> float: # 标准编辑距离,按字符算 m, n = len(ref), len(hyp) dp = list(range(n + 1)) for i in range(1, m + 1): prev, dp[0] = dp[0], i for j in range(1, n + 1): cur = dp[j] cost = 0 if ref[i - 1] == hyp[j - 1] else 1 dp[j] = min(dp[j] + 1, dp[j - 1] + 1, prev + cost) prev = cur return dp[n] / max(m, 1) # 逐页比对,按 CER 倒序输出最差页面 scores = [(p, cer(ref_map[p], hyp_map[p])) for p in ref_map] for path, score in sorted(scores, key=lambda x: -x[1])[:10]: print(f'{score:.4f}\t{path}')逻辑说明:编辑距离按字符级别算,中文不需要分词。参数说明:分母取参考文本长度,空文本要单独处理避免除零;如果业务只关心关键字段,可以把 CER 换成字段级命中率,更贴近交付标准。排序后看最差十页,通常能发现是某一类版面或某一种字体在系统性出错,而不是随机噪声。
验收还有一步是抽样复核。我的习惯是按 5% 比例随机抽页,人工对照原图检查,重点看数字、日期、编号这些不能错的内容。抽样发现的问题要回写到预处理规则或纠错表里,形成闭环。别指望一次调参就完美,OCR 项目的质量是靠“跑一批、看一批、修一批”磨出来的。
从那以后我每次接文档识别需求,都会先要一份带人工标注的小样本,把 CER 和字段准确率跑出来再谈排期,而不是拍脑袋承诺识别率。模型再强也替代不了验收标准,这条经验帮我省过不少返工。希望帮到你。
本文还有配套的精品资源,点击获取