简介:面向需要在本地部署文字识别能力的开发者与办公自动化用户,这份资源提供Tesseract-OCR的完整安装包及中文语言包,可用于扫描文档、图片文字提取、批量数据录入等场景,也适合研究OCR训练流程的技术人员。压缩包共724个文件、约36.01MB,以C/C++源码(h、cpp)为主体,另含Java、XML配置、Shell脚本及am、traineddata等语言与训练数据,并附命令行工具手册、示例图片和训练辅助文件,兼顾直接安装与二次开发。已有289人学习下载。读者可从中获得可运行的开源OCR环境、中文识别数据,以及字符集提取、聚类、训练与词典转换等配套脚本和说明,便于把OCR能力集成进自有程序,或按业务语料调整识别模型;对排查安装、语言包配置和识别效果问题也有参考价值。
1. 为什么命令行里还留着 Tesseract:一个离线 OCR 的落点
扫描件要转文字,很多人的第一反应是调云端接口。但只要碰上内网环境、票据批量归档、或者每天上万张图不想按次计费的场景,Tesseract-OCR 这个老牌开源光学字符识别引擎就会被重新翻出来。它不依赖网络,装完就能离线跑,配上中文语言包 chi_sim 足以应付大部分印刷体中文。这套完整安装包加语言包的组合,解决的就是“拿到一台干净机器,半小时内把 OCR 跑起来”的问题。它更像一个能被任意语言调用的命令行插件,而不是带界面的软件,所以真正的工作量在参数和预处理上。适合做文档数字化的后端开发、做票据证照识别的算法同学,以及要把识别环节塞进本地流水线的运维。下面按“装什么、怎么调、在哪翻车”的顺序拆。
2. 安装包与语言包怎么选:版本、目录结构与 chi_sim 的落地位置
很多人卡在第一步不是因为不会装,而是装完了--list-langs里没有 chi_sim,或者有 chi_sim 但识别出来全是问号和空格。这一章把安装包里到底装了什么、语言包该放哪儿、环境变量该指向谁讲清楚,后面调参才有意义。
2.1 版本选型:4.x 与 5.x 的实际差别
Tesseract 从 4.0 开始引入 LSTM 识别引擎,5.x 进一步把 LSTM 做成默认且唯一的默认路径。对中文来说这个差别很实在:3.x 时代用的还是基于特征模板的老引擎,中文识别基本处于“能出字但不能用”的水平;4.x 之后用 chi_sim 的 LSTM 模型,规整的印刷体可以做到 90% 以上的字准确率。
如果你拿到的安装包是 5.x,--oem的可用取值如下:0 是仅 legacy 引擎,1 是仅 LSTM,2 是两者混合,3 是默认按可用性自动选。5.x 里 legacy 引擎在编译时可能已经被关掉,这时候传--oem 0会直接报错退出,这个报错很多人第一次见会以为安装包坏了。
模型文件本身也分三档,选错了不是不能跑,是跑得慢或者不准:
| 模型类型 | 体积量级 | 速度 | 准确率 | 适用场景 |
|---|---|---|---|---|
| tessdata_fast | 小 | 最快 | 一般 | 实时预览、大批量粗筛 |
| tessdata(标准) | 中 | 中等 | 较好 | 绝大多数生产场景 |
| tessdata_best | 大 | 慢 3~5 倍 | 最好 | 归档级数字化、离线批处理 |
我一般会给中文场景配标准版 chi_sim,再单独留一份 best 版做回溯校验。fast 版中文在小字号上掉点很明显,不建议直接上生产。
2.2 目录结构与 TESSDATA_PREFIX 到底指向谁
安装完成后,核心目录只有一个tessdata,里面平铺着一堆*.traineddata文件,每个文件就是一种语言或一份配置。chi_sim.traineddata是简体中文,chi_sim_vert.traineddata是竖排简体,chi_tra是繁体,eng是英文,osd.traineddata负责文字方向和脚本检测——这个文件经常被漏掉,结果就是带旋转的扫描件识别结果整段错位。
TESSDATA_PREFIX是这块最容易翻车的地方。不同版本的语义不完全一致:多数情况下它应该指向tessdata的父目录,而部分构建里又允许直接指向tessdata目录本身。与其背结论,不如让程序自己告诉你:
# --list-langs 会把实际搜索的语言包目录打印出来 tesseract --list-langs # 输出形如: # List of available languages in "/usr/share/tesseract-ocr/5/tessdata/" (3): # chi_sim # chi_sim_vert # eng括号里的路径就是它真正在找的位置。把chi_sim.traineddata放进这个目录,比折腾环境变量可靠得多。如果确实需要用自定义目录,可以用--tessdata-dir显式指定,优先级高于环境变量:
tesseract scan.png stdout -l chi_sim --tessdata-dir /data/ocr/tessdata注意:
--tessdata-dir指向的是tessdata目录本身,不含末尾斜杠,也不要再往下指一层。
2.3 三个平台的落地步骤
Linux 和 macOS 用包管理器最省事,语言包会被自动放到正确位置:
# Debian / Ubuntu 系 sudo apt-get update sudo apt-get install -y tesseract-ocr tesseract-ocr-chi-sim tesseract-ocr-chi-sim-vert # macOS brew install tesseract brew install tesseract-lang # 一次性装齐所有语言包,体积偏大 # 验证 tesseract --version tesseract --list-langsWindows 走安装包的话,安装向导里有一个“Additional language data”勾选项,勾上之后会把一批语言包一起放进去;如果安装包是精简版没带中文,就手动把chi_sim.traineddata拷进安装目录下的tessdata。默认路径通常是C:\Program Files\Tesseract-OCR\tessdata,注意这个目录在 Program Files 下,普通用户权限写不进去,需要管理员权限复制。
一个常被忽略的细节:从网页下载.traineddata文件时,浏览器可能给它加上.txt后缀,变成chi_sim.traineddata.txt。Tesseract 只认后缀严格匹配的文件,多一个.txt就等于没装,而且--list-langs里也不会出现 chi_sim,很容易误判成语言包本身有问题。
2.4 用一张图确认整条链路是通的
装完别急着写代码,先用命令行跑一张图,把“引擎→语言包→输出”这条链路验一遍:
# 生成一张测试图(有现成图片就跳过这步) # 识别:-l 指定语言,--psm 6 假定是一整块文本 tesseract test.png stdout -l chi_sim --psm 6能正常输出中文,说明安装包、语言包、路径全对。这一步没跑通就去调 Python,等于在错误的基线上排错,血泪经验。
3. 跑通第一条识别链路:命令行参数与 Python 封装
命令行能出字只是起点。生产里真正决定成败的是三个参数:语言、版面切分模式(psm)、引擎模式(oem)。把它们理解成“告诉引擎这张图长什么样”,比死记取值有用得多。
3.1 --psm:告诉引擎这张图的版面长什么样
--psm取值 0 到 13,最常用的就那么几个。默认值是 3(全自动分页),它在整页文档上表现尚可,但在截图、票据、单行文字上经常把版面切得七零八落。
| psm | 含义 | 典型用法 |
|---|---|---|
| 3 | 全自动分页,无方向检测 | 整页扫描件默认值 |
| 4 | 假定单列、字号可变 | 单栏文章、说明书 |
| 6 | 假定单个统一文本块 | 票据正文、截图段落 |
| 7 | 假定单行文本 | 标题、表头、单行字段 |
| 8 | 假定单个词 | 关键词提取 |
| 11 | 稀疏文本,尽可能找更多字 | 海报、带图排版、 scattered 文字 |
| 13 | 原始单行,跳过内部处理 | 已做过精细预处理的单行 |
实际用法:
# 单行字段:发票号、金额、姓名 tesseract field.png stdout -l chi_sim --psm 7 # 一段正文:截图里的说明文字 tesseract block.png stdout -l chi_sim --psm 6 # 海报、菜单这类文字分散的图 tesseract poster.png stdout -l chi_sim --psm 11--psm 6和--psm 3在截图上的差距经常是“能读”和“读成一堆碎片”的差距。判断方法很粗暴:如果你知道图里只有一段连续文字,就别让它自己去猜版面。
3.2 Python 侧的最小封装
Python 生态里最常用的是 pytesseract,它本质上是给命令行套了个壳,所以命令行的所有参数都能透传。
import pytesseract from PIL import Image # Windows 下 pytesseract 不会自动找到 exe,必须显式指定 # Linux/macOS 一般已在 PATH 里,这行可以省掉 pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" img = Image.open("invoice.png") text = pytesseract.image_to_string( img, lang="chi_sim", # 语言,多语言用加号:chi_sim+eng config="--psm 6 --oem 1 " # 版面模式 + 引擎模式 "-c preserve_interword_spaces=1", # 保留词间空格,表格类有用 ) print(text)三个地方值得说清楚。lang="chi_sim+eng"这种写法是允许的,中英混排的票据建议加上 eng,否则纯数字和英文单词容易被中文模型“脑补”成汉字。--oem 1强制走 LSTM,避免某些版本在低质量图上自动切回 legacy 导致结果不稳定。preserve_interword_spaces=1对表格和表单很关键,默认情况下 Tesseract 会把连续空格压缩掉,还原出来的文本就没法按列对齐了。
如果系统里装了多个版本,或者 tessdata 在非标准位置,加上--tessdata-dir比改环境变量更可控,尤其是在容器里跑的时候。
3.3 拿到词级坐标和置信度
只要纯文本是不够的,业务上通常需要知道“这个字在哪、有多可信”。image_to_data返回的是词级结构:
data = pytesseract.image_to_data( img, lang="chi_sim", config="--psm 6", output_type=pytesseract.Output.DICT, ) # 关键字段:text 词内容、conf 置信度(0-100,-1 表示非文本行) for i, word in enumerate(data["text"]): word = word.strip() conf = int(data["conf"][i]) if word and conf > 60: print(f"{word}\tconf={conf}\t" f"box=({data['left'][i]},{data['top'][i]}," f"{data['width'][i]},{data['height'][i]})")conf低于 60 的词基本可以认为不可信,这个阈值不是拍脑袋:中文场景下 60 以下大概率是粘连、断笔或者被误判为文字的图形噪声。把低置信度词单独捞出来做人工复核,比整页返工效率高一个量级。level字段区分页/块/段/行/词五级,需要按行聚合时筛level == 4即可。
4. 图像预处理与版面分析:让识别率从 60% 到 90% 的具体操作
同一张图,直接喂和预处理后喂,中文识别率差 30 个点很常见。Tesseract 内部没有做太多图像增强,它假定输入已经是“干净的文档图像”。这个假定在扫描件和手机拍照上几乎从不成立。
4.1 x-height 是隐形硬指标
Tesseract 对字符高度有隐含要求:小写字母主体的高度(x-height)最好在 30 像素以上,对应整页扫描的 300 DPI 左右。低于这个值,LSTM 的特征提取会明显退化,表现就是“字都认识但拼不对”,或者干脆输出空字符串。
这就是为什么很多截图直接识别效果很差——手机截屏的逻辑分辨率可能只有 700 像素宽,一行 20 个字,每个字高度不到 20 像素。解决办法是把图放大,不是裁剪。我一般按短边缩放到 1000~1500 像素作为基准,宁可大一点。
4.2 一条能抄的预处理流水线
下面这段是长期用的默认流程,灰度→放大→去噪→二值化→加白边,五步都有明确目的:
import cv2 import numpy as np def preprocess(path, min_side=1200): img = cv2.imread(path, cv2.IMREAD_COLOR) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 1. 等比放大:短边不足 min_side 时,字符高度往往不到 30px h, w = gray.shape[:2] if min(h, w) < min_side: scale = min_side / float(min(h, w)) gray = cv2.resize(gray, None, fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC) # 2. 中值滤波去椒盐噪声,核最大 3,再大就把笔画糊掉了 gray = cv2.medianBlur(gray, 3) # 3. 自适应二值化:应对拍照时的光照不均,比全局 Otsu 稳 binary = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blockSize=31, # 必须是奇数,约为字符高度的 1.5~2 倍 C=10, # 越大越“狠”,噪声多时往上调 ) # 4. 加白边:贴边字符经常被整行漏掉 binary = cv2.copyMakeBorder(binary, 20, 20, 20, 20, cv2.BORDER_CONSTANT, value=255) # 5. 白字黑底要反相,Tesseract 只认黑字白底 if np.mean(binary) < 127: binary = cv2.bitwise_not(binary) return binaryblockSize和C是最需要按图调的两个参数。blockSize取太小,二值化会把笔画内部也当成背景,字变成空心;取太大,光照渐变就压不平。经验值是字符高度的 1.5~2 倍,先估一下图里一个字大概多少像素,再定这个值。C是从均值里减掉的偏置,噪声多的手机拍照往上调到 15 左右,干净的扫描件保持 10 甚至 5 就行。
如果原图对比度本来就很好(比如电子版 PDF 转的图),直接 Otsu 全局阈值反而更干净:
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)判断标准很简单:整张图光照均匀就用 Otsu,有阴影、有反光、有渐变就用自适应。
4.3 倾斜校正:几度的问题能毁掉一整页
扫描件放歪 2 度,长行的末尾就会偏出十几像素,行切分直接失败,输出变成一行一个字。检测倾斜角有个轻量做法:
def deskew(binary): # 取所有前景像素坐标,拟合最小外接矩形 coords = np.column_stack(np.where(binary < 128)) if len(coords) < 50: return binary angle = cv2.minAreaRect(coords)[-1] # OpenCV 返回的角度需要归一化到 [-45, 45] if angle < -45: angle = 90 + angle if abs(angle) < 0.3: # 角度太小就别转了,重采样反而引入模糊 return binary h, w = binary.shape[:2] m = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) return cv2.warpAffine(binary, m, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)这个方法的边界要说清楚:它依赖前景像素的整体分布,遇到带大量插图的页面或者多栏排版会算歪。纯文字页、票据、表单用它没问题;图文混排的页面建议先切出文字区域再做。角度小于 0.3 度时不要转,重采样带来的模糊损失比那点倾斜更大。
4.4 版面切分:有时候切图比调参有效
一张 A4 上同时有表头、表格、页脚说明,交给--psm 3去自动分页,结果经常是表格和正文被揉成一团。我的做法是先按业务语义切 ROI,每块用最合适的 psm 单独识别,最后按坐标合并。
# 按列切表格:先定位竖线,再逐列识别 def split_columns(binary, n=3): h, w = binary.shape[:2] step = w // n return [binary[:, i * step:(i + 1) * step] for i in range(n)] for idx, col in enumerate(split_columns(binary, 3)): txt = pytesseract.image_to_string( col, lang="chi_sim", config="--psm 6" ) print(f"--- 第 {idx + 1} 列 ---") print(txt)切的时候一定要留重叠区,字符被切一半是最难查的错误——识别结果里出现莫名其妙的半个字,八成是切图边界压到笔画了。每块左右各留 10~20 像素冗余,代价很小。
5. 避坑与排查:五条踩过的坑,按现象对号入座
这一章的五条都是我或同事真实遇到过的,按“现象→原因→解决”写,遇到问题可以直接对号。
5.1 报错找不到 tesseract 可执行文件
现象:Python 里一调用就抛TesseractNotFoundError,提示tesseract is not installed or it's not in your PATH,但命令行里敲tesseract --version明明有输出。
原因:pytesseract 启动的是子进程,它按 PATH 找可执行文件。Windows 上安装包默认不会把目录加进 PATH,或者在 IDE 里启动时继承的环境变量和终端不一致。
解决:显式指定绝对路径,别依赖 PATH。pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"。路径里有空格一定要用原始字符串,反斜杠转义问题会让你怀疑人生。容器里则建议在 Dockerfile 里做软链到/usr/local/bin。
5.2 语言包装了但识别出来是空的
现象:--list-langs里能看到 chi_sim,但识别结果只有空白或者零星几个字符。
原因:三种可能——图片本身字符太小(见 4.1);语言包文件下载不完整,.traineddata只有几百 KB(正常中文包几十 MB);或者文件后缀被加了.txt。
解决:先比一下文件体积,明显偏小就是没下完。再用命令行直接跑,排除 Python 层的干扰。如果是小字问题,把图放大两倍再试,能出字就说明是分辨率问题而不是安装问题。
5.3 中文被识别成一行一个字
现象:输出的文本每个汉字占一行,原本的一段话被拆得七零八落。
原因:--psm选错了。默认的 3 在窄图或者被裁得很紧的图上会判断成多列,行切分失败后退化成逐字输出。另一种情况是图片被过度二值化,字与字之间的间隔被放大。
解决:先试--psm 6,单块文本;再试--psm 7,单行。如果仍然一个字一行,回退二值化参数,把blockSize调大、C调小,让笔画连回去。
5.4 竖排中文完全识别不出来
现象:古籍、招牌、部分证照是竖排的,识别结果全是乱码或者干脆空。
原因:默认的 chi_sim 是按横排训练的,竖排需要单独的chi_sim_vert.traineddata。
解决:装上竖排语言包,用-l chi_sim_vert调用,同时版面模式选--psm 5(假定单个统一垂直文本块)。如果图是横排但被旋转了 90 度,不要用竖排模型硬扛,先用osd.traineddata检测方向再旋正,效果差得远。
5.5 首次调用特别慢,后面就快了
现象:服务刚启动时第一张图要一两秒,后面每张只要几十毫秒。
原因:LSTM 模型是懒加载的,第一次调用才把.traineddata读进内存。中文标准模型的体积不小,冷启动开销明显。
解决:在服务启动阶段做一次预热,拿一张几十像素的小图跑一遍image_to_string,把模型顶进内存。如果是容器部署,别在每次请求里重新创建子进程,尽量复用进程。多语言切换时注意每套模型都会各自占内存,chi_sim+eng是两个模型同时在跑。
6. 进阶:批量流水线里的置信度闸门与抽样复核
单张图调通之后,真正的分水岭在批量。我做过一个票据归档的活儿,两万张图,一开始全量跑完直接入库,结果下游对账时发现金额字段错了上百条。后来改成带闸门的流水线,返工量直接压到个位数。
核心思路是:别把 OCR 当“一定对”的黑匣子,把它当“带置信度的候选答案”,用置信度决定哪些结果可以直接用、哪些必须复核。
import os import csv import pytesseract from PIL import Image pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" rows = [] for name in os.listdir("./input"): if not name.lower().endswith((".png", ".jpg", ".jpeg")): continue img = Image.open(os.path.join("./input", name)) data = pytesseract.image_to_data( img, lang="chi_sim", config="--psm 6", output_type=pytesseract.Output.DICT, ) words = [(w.strip(), int(c)) for w, c in zip(data["text"], data["conf"]) if w.strip() and int(c) >= 0] if not words: rows.append((name, "", 0, "空结果")) continue # 用词数加权算整页平均置信度 avg = sum(c for _, c in words) / len(words) text = "".join(w for w, _ in words) status = "可用" if avg >= 75 else ("复核" if avg >= 50 else "拒收") rows.append((name, text, round(avg, 1), status)) with open("result.csv", "w", newline="", encoding="utf-8-sig") as f: csv.writer(f).writerows(rows)阈值怎么定不是玄学:先拿两三百张有标注的图跑一遍,画出置信度分布,找到“准确率开始明显掉”的那个拐点。中文印刷体一般在 75 附近,手写和低质量扫描件会低到 55 左右。定完阈值别当铁律,业务字段不同阈值也该不同——金额、证件号这种错一个字符就出事的字段,单独拉高阈值甚至强制全量复核。
另外两个小技巧。一是把--psm按图片类型分组,别一张配置打天下,票根和整页文档的最佳 psm 本来就不一样。二是记录每张图的耗时和二值化参数,出问题时能回溯是哪一步变了——这套日志比事后猜有用得多,算是给流水线留了个黑匣子。
从那以后我每次接 OCR 相关的活儿,都强制先跑一遍置信度分布再上线,哪怕客户催得再急。便宜的药方往往最贵。希望帮到你。
本文还有配套的精品资源,点击获取