1. 为什么我最终把票投给了 easyocr
做图像相关项目的朋友大概都有过这种经历:手头有一批截图、扫描件或者手机拍的照片,需要把里面的文字提取出来。第一反应可能是去找在线OCR服务,传上去、等结果、复制下来,偶尔还要担心文件隐私。用着用着就发现,网络一卡、文件一大、格式一怪,体验立刻断崖式下跌。更别提有些场景根本不允许把图片往外传,比如内部资料整理、个人证件信息归档这类需求。
我最早接触OCR是从一些老牌开源引擎开始的,装起来倒是不难,但识别中文的时候那个准确率实在让人挠头,尤其是遇到倾斜、光照不均、字体花哨的图片,出来的结果基本没法直接用。后来也试过一些云端API,效果确实好,但免费额度用完就得掏钱,长期用下来成本不低,而且每次调用都要走网络,批量处理几百张图的时候那个等待时间相当煎熬。
直到用上 easyocr,才算找到了一个比较舒服的平衡点。它的定位很明确:装起来简单、用起来简单、识别效果还过得去。底层是基于深度学习的文字检测和识别模型,支持八十多种语言,中文、英文、日文、韩文这些常见语种都在列,而且不需要你手动做图像预处理,扔进去一张图,它自己会搞定检测框定位和文字识别两件事。对于我这种不想在环境配置上折腾太久、又希望有个稳定离线方案的人来说,easyocr 基本就是那个“够用且省心”的选择。
这篇文章我会从实际使用的角度出发,把 easyocr 的安装、核心用法、参数调优、常见坑点以及一些进阶玩法都聊一遍。不管你是刚接触OCR的新手,还是已经用过其他方案想换个工具试试的老手,应该都能从中找到对自己有用的东西。文章里涉及的操作步骤和参数配置,都是我反复实测之后总结出来的,你可以直接照着抄作业。
2. easyocr 的安装与首次运行:比想象中少踩很多坑
2.1 环境准备:Python 版本和依赖管理
easyocr 对 Python 版本的要求不算苛刻,3.7 到 3.11 都能跑,但我个人建议用 3.8 或 3.9,这两个版本在各类深度学习库的兼容性上最稳。如果你用的是 3.11 以上,某些依赖包的预编译轮子可能还没跟上,装的时候容易卡在编译环节。
安装方式有两种,看你的使用场景来选:
- pip 直接装:适合快速上手,一条命令搞定。
- 源码安装:适合需要改源码或者跟进最新特性的情况。
绝大多数人用第一种就够了。打开终端,执行:
pip install easyocr这条命令会自动把 torch、torchvision、opencv-python、Pillow 这些依赖一起拉下来。这里有个细节要注意:easyocr 默认装的是 CPU 版的 torch。如果你有 NVIDIA 显卡并且想用 GPU 加速,需要先去 pytorch 官网找到对应 CUDA 版本的安装命令,手动装好 GPU 版 torch,然后再装 easyocr。否则即使你有显卡,easyocr 也只会用 CPU 跑,速度差距大概在五到十倍左右。
怎么判断自己装的是 CPU 版还是 GPU 版?在 Python 里跑一行:
import torch print(torch.cuda.is_available())输出True就是 GPU 版可用,False就是 CPU 版或者 CUDA 环境没配好。如果是False但你有显卡,那就得重新装 torch。
提示:如果你在公司内网或者网络受限的环境下安装,可能会遇到下载超时的问题。可以配置国内镜像源来加速,比如清华源或者阿里源,具体命令网上很多,这里不展开。
2.2 模型下载:第一次运行时的“隐藏步骤”
easyocr 装好之后,第一次调用Reader的时候会自动下载检测模型和识别模型。检测模型负责找出图片里哪里有文字,识别模型负责把文字内容读出来。这两个模型加起来大概一百多兆,下载速度取决于你的网络环境。
这里有个很多人会忽略的点:模型下载是一次性的,下载完之后会缓存在本地。默认路径在用户目录下的.EasyOCR/model文件夹里。如果你在多台机器上部署,可以把下载好的模型文件拷贝过去,省去重复下载的时间。具体路径可以用这段代码查:
import easyocr reader = easyocr.Reader(['ch_sim', 'en']) print(reader.model_storage_directory)输出就是模型存放的位置。把整个model文件夹复制到目标机器的对应位置,就能跳过下载步骤。
另外,如果你在离线环境下工作,也可以手动下载模型文件然后放到指定目录。模型文件的下载地址在 easyocr 的官方文档里有说明,这里就不贴具体链接了,需要的朋友可以自行查阅。
2.3 跑通第一个例子:从图片到文字的完整链路
环境准备好之后,来跑一个最简单的例子。假设你有一张名为test.png的图片,里面有一段中英文混合的文字:
import easyocr reader = easyocr.Reader(['ch_sim', 'en']) result = reader.readtext('test.png') for item in result: print(item)readtext返回的是一个列表,每个元素是一个元组,包含三个部分:边界框坐标、识别出的文字、置信度。边界框是一个四个点的坐标列表,文字就是识别结果,置信度是一个 0 到 1 之间的浮点数,越接近 1 表示模型越有把握。
如果你只想拿文字内容,可以这样写:
texts = [item[1] for item in result] print('\n'.join(texts))第一次运行的时候会看到终端里出现下载进度条,等模型下载完,识别结果就出来了。整个过程不需要你做任何图像预处理,easyocr 内部会自动完成灰度化、尺寸调整、归一化这些操作。
实测下来,对于清晰的截图和扫描件,easyocr 的中英文识别准确率相当不错,基本能到 95% 以上。对于手机拍摄的照片,如果光线均匀、文字没有明显倾斜,效果也还可以。真正考验它的是那些模糊、倾斜、背景复杂的图片,这个后面会专门讲怎么调优。
3. Reader 对象的参数怎么调:不是越多越好,而是要配对
3.1 语言参数:一次加载多种语言的代价与收益
Reader构造函数的第一个参数是语言列表。easyocr 支持的语言很多,常见的有:
| 语言代码 | 对应语言 |
|---|---|
| ch_sim | 简体中文 |
| ch_tra | 繁体中文 |
| en | 英文 |
| ja | 日文 |
| ko | 韩文 |
| fr | 法文 |
| de | 德文 |
| es | 西班牙文 |
你可以同时传多个语言,比如['ch_sim', 'en']表示同时识别简体中文和英文。这里有个重要的经验:语言组合会影响识别速度和准确率。每多加载一种语言,识别模型就会变大,推理时间也会增加。而且某些语言之间的字符集有重叠,组合不当反而会降低准确率。
我的建议是:只加载你实际需要的语言。如果你只处理中文文档,就只传['ch_sim'],不要顺手把英文也加上。中英文混合的场景确实需要同时加载两个,但日文和韩文如果不同时出现,就没必要一起加载。
还有一个细节:ch_sim和ch_tra不要同时加载。这两个模型的字符集高度重叠,同时加载会导致模型在判断某个字是简体还是繁体时产生混淆,实测准确率会下降。如果你的图片里简繁体都有,优先选ch_sim,它对繁体字的兼容性比想象中好。
3.2 GPU 开关:什么时候该开,什么时候不该开
Reader的gpu参数控制是否使用 GPU 加速。默认值是True,但前提是你装了 GPU 版的 torch。如果你只有 CPU,这个参数设成True也不会报错,只是会自动回退到 CPU 模式,但会多花一点检查时间。
reader = easyocr.Reader(['ch_sim', 'en'], gpu=True)什么时候该开 GPU?批量处理大量图片的时候。单张图片的识别时间在 CPU 上大概是 1 到 3 秒,GPU 上可以降到 0.2 到 0.5 秒。如果你要处理几百上千张图,这个差距就非常明显了。
但如果你只是偶尔识别一两张图,CPU 完全够用,没必要折腾 GPU 环境。而且 GPU 模式下显存占用会随着图片尺寸增大而增加,处理超大图的时候可能遇到显存不足的问题,这时候反而要回退到 CPU 或者先把图片缩小。
注意:如果你在服务器上部署,而服务器有多张显卡,可以通过
CUDA_VISIBLE_DEVICES环境变量指定用哪张卡,避免和其他任务抢资源。
3.3 其他实用参数:verbose、model_storage_directory 等
Reader还有几个参数值得关注:
- verbose:默认
True,会打印模型加载和下载的日志。在生产环境里建议设成False,避免日志刷屏。 - model_storage_directory:指定模型存放路径。多项目共用模型的时候很有用,可以避免重复下载。
- download_enabled:默认
True,如果设成False,模型不存在时不会自动下载,而是直接报错。离线部署的时候用得上。 - user_network_directory:自定义识别网络的存放路径,进阶用户才会用到。
这些参数不需要一开始就全部搞懂,用到的时候查一下就行。我的习惯是先把verbose=False加上,让输出干净一点。
4. 识别效果不理想时,我从哪些方向入手排查
4.1 图片预处理的取舍:easyocr 帮你做了哪些,没做哪些
easyocr 内部确实做了一些预处理,比如把图片转成 RGB、调整尺寸、归一化像素值。但它不会帮你做去噪、二值化、纠偏这些操作。如果你的图片质量很差,指望 easyocr 直接输出完美结果是不现实的。
我遇到过几种典型的“识别效果差”场景:
- 图片分辨率太低:文字只有十几个像素高,模型根本看不清。这种情况再怎么调参数都没用,只能换图或者放大图片。
- 文字倾斜严重:比如拍摄角度偏了三十度以上,检测框会歪,识别准确率骤降。
- 背景复杂:文字压在花纹、渐变、水印上面,检测模型容易漏检或者框错位置。
- 光照不均:一半亮一半暗,暗处的文字识别不出来。
针对这些情况,我的处理策略是:先判断问题出在检测还是识别。如果检测框都没框对,那问题在检测阶段,需要做图像增强;如果框对了但文字读错了,那问题在识别阶段,可以尝试调整识别参数或者换语言模型。
一个简单的判断方法:把readtext返回的边界框画到原图上,看看框的位置准不准。如果框是准的,那识别错误大概率是图片清晰度或者字体的问题。
4.2 调整 text_threshold 和 low_text:控制检测灵敏度的两个旋钮
readtext方法支持传入一些检测相关的参数,其中两个最常用:
- text_threshold:文字置信度阈值,默认 0.7。调低会让更多区域被判定为文字,适合文字比较淡的图片;调高会过滤掉一些不确定的区域,适合背景干扰多的图片。
- low_text:低置信度文字的阈值,默认 0.4。这个参数控制的是“边缘文字”的判定,调低可以保留更多模糊文字,但可能引入噪声。
result = reader.readtext('test.png', text_threshold=0.6, low_text=0.3)这两个参数需要配合使用。我的经验是:如果漏检严重,先把 text_threshold 降到 0.5 试试;如果误检太多,把它升到 0.8。low_text 一般不用动,除非你处理的是手写体或者艺术字。
还有一个参数叫link_threshold,默认 0.4,控制的是文字区域之间的连接。对于字间距很大的文本,调低这个值有助于把同一行的文字连起来。但这个参数比较敏感,调不好反而会把不该连的区域连在一起,建议谨慎使用。
4.3 处理大图时的分块策略:避免显存溢出和精度下降
easyocr 对输入图片的尺寸没有硬性限制,但图片太大会导致两个问题:显存占用飙升和小文字识别精度下降。因为模型在推理前会把图片缩放到固定尺寸,如果原图很大,缩放后小文字就变得模糊了。
我的做法是:如果图片宽度超过 2000 像素,先切块再识别。切块的方式有两种:
- 按固定尺寸切:比如每块 1000x1000,块之间留 100 像素重叠,避免文字被切断。
- 按内容切:先做一次粗略检测,找到文字密集区域,再针对这些区域切块。
第一种方式实现简单,适合大多数场景。第二种方式更精细,但实现复杂度高,适合对精度要求极高的场景。
切块之后,每块单独调用readtext,最后把结果按坐标合并。合并的时候要注意去重,因为重叠区域可能会被识别两次。
import cv2 img = cv2.imread('large_image.png') h, w = img.shape[:2] block_size = 1000 overlap = 100 results = [] for y in range(0, h, block_size - overlap): for x in range(0, w, block_size - overlap): block = img[y:y+block_size, x:x+block_size] block_results = reader.readtext(block) for bbox, text, conf in block_results: # 把局部坐标转成全局坐标 global_bbox = [[p[0]+x, p[1]+y] for p in bbox] results.append((global_bbox, text, conf))这段代码只是示意,实际用的时候还要处理边界情况和去重逻辑。但思路就是这样:分而治之,最后合并。
5. 批量处理和性能优化:从单张到上千张的实战经验
5.1 批量识别的代码骨架:循环、进度条和异常捕获
单张识别跑通之后,下一步就是批量处理。最朴素的写法就是一个 for 循环:
import os import easyocr reader = easyocr.Reader(['ch_sim', 'en'], verbose=False) image_dir = './images' output_dir = './results' os.makedirs(output_dir, exist_ok=True) for filename in os.listdir(image_dir): if not filename.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp')): continue filepath = os.path.join(image_dir, filename) try: result = reader.readtext(filepath) texts = [item[1] for item in result] with open(os.path.join(output_dir, filename + '.txt'), 'w', encoding='utf-8') as f: f.write('\n'.join(texts)) except Exception as e: print(f'处理 {filename} 时出错: {e}')这个骨架够用,但有几个地方可以优化:
- 进度显示:用
tqdm库加个进度条,处理大量文件的时候心里有数。 - 异常捕获:单张图片出错不应该中断整个批次,用 try-except 包起来,记录出错的文件名,事后单独处理。
- 结果格式:除了纯文本,还可以把边界框和置信度一起保存成 JSON,方便后续分析。
5.2 多进程加速:什么时候值得上,什么时候反而更慢
Python 的 GIL 限制了多线程的并行能力,所以批量处理想加速,一般用多进程。multiprocessing或者concurrent.futures.ProcessPoolExecutor都可以。
但这里有个坑:每个进程都会加载一份模型。如果你的模型是中文加英文,一份大概占几百兆内存,开四个进程就是一千多兆。内存不够的机器直接卡死。而且模型加载本身也要时间,如果每个进程处理的任务很少,加载模型的开销可能比识别本身还大。
我的经验是:只有当单批任务超过 50 张图片,且机器内存充足(至少 8GB 以上)时,才考虑多进程。否则单进程慢慢跑反而更稳。如果确实需要加速,可以考虑把模型加载放在进程初始化阶段,让每个进程只加载一次模型,然后循环处理分配给它的任务。
from concurrent.futures import ProcessPoolExecutor import easyocr def init_reader(): global reader reader = easyocr.Reader(['ch_sim', 'en'], verbose=False) def process_image(filepath): result = reader.readtext(filepath) return [item[1] for item in result] # 使用 initializer 让每个进程只加载一次模型 with ProcessPoolExecutor(max_workers=4, initializer=init_reader) as executor: futures = [executor.submit(process_image, fp) for fp in file_list] for future in futures: print(future.result())这段代码的关键是initializer参数,它确保每个子进程在开始处理任务之前先加载好模型,避免重复加载的开销。
5.3 内存和显存监控:别让程序跑着跑着就崩了
批量处理最怕的就是跑了一半程序崩了,前面的结果全丢。除了做好异常捕获和断点续跑,还要监控内存和显存的使用情况。
在 Linux 上可以用htop或者nvidia-smi实时看。在 Python 里可以用psutil查内存,用torch.cuda.memory_allocated()查显存。
import psutil import torch print(f'内存使用: {psutil.Process().memory_info().rss / 1024 / 1024:.1f} MB') if torch.cuda.is_available(): print(f'显存使用: {torch.cuda.memory_allocated() / 1024 / 1024:.1f} MB')如果发现内存持续增长不释放,可能是图片对象或者中间结果没有及时回收。可以在每处理完一批之后手动调用gc.collect(),或者把处理逻辑封装成函数,让局部变量自然释放。
还有一个容易忽略的点:OpenCV 读取的图片是 BGR 格式,而 easyocr 期望的是 RGB 格式。如果你用cv2.imread读图然后直接传给readtext,颜色通道是反的,虽然大多数时候不影响识别,但在某些颜色对比度低的图片上可能会导致检测失败。稳妥的做法是手动转换:
img = cv2.imread('test.png') img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) result = reader.readtext(img_rgb)或者直接用PIL读图,它默认就是 RGB 格式。
6. 那些文档里不会写的踩坑记录
6.1 模型下载失败和缓存混乱的处理
前面提到过模型会自动下载,但实际用的时候可能会遇到下载失败的情况。常见原因有两个:网络不稳定和缓存目录权限不对。
如果下载到一半断了,重新运行的时候 easyocr 会尝试续传,但有时候缓存文件损坏了,它会一直卡在那里。这时候需要手动把.EasyOCR/model目录下的临时文件删掉,重新下载。
还有一种情况是缓存目录混乱:比如你先装了英文模型,后来又装了中文模型,两个模型的文件名可能有冲突。easyocr 的模型命名规则是按语言代码来的,正常情况下不会冲突,但如果你手动拷贝过模型文件,就可能出现版本不匹配的问题。这时候最简单的办法是把整个model目录删掉,让它重新下载。
提示:如果你在 Docker 容器里跑 easyocr,记得把模型目录挂载到宿主机上,否则每次重建容器都要重新下载模型,浪费时间。
6.2 中文识别中的标点符号和空格问题
easyocr 识别中文的时候,标点符号的处理有点意思。它会把中文标点识别成对应的全角符号,比如逗号识别成“,”,句号识别成“。”,这没问题。但有时候它会把英文标点也识别成全角,比如把“.”识别成“。”,这在处理中英文混合文本的时候会造成困扰。
空格的处理也有类似问题。中文文本里通常没有空格,但 easyocr 有时候会在汉字之间插入空格,尤其是当字间距比较大的时候。如果你后续要做文本分析,这些多余的空格需要清理掉。
我的处理方式是:识别结果先不做任何修改,保存原始输出;然后在后处理阶段根据具体需求做清洗。比如做中文分词之前,先把连续空格替换成单个空格,再把中文字符之间的空格去掉。这样既保留了原始信息,又能得到干净的文本。
6.3 竖排文字和特殊字体的识别表现
easyocr 对横排文字的识别效果最好,竖排文字也能识别,但准确率会下降。如果你处理的是古籍或者某些特殊排版的文档,竖排文字比较多,可能需要额外的处理:先把图片旋转 90 度,让文字变成横排,再识别。
特殊字体方面,印刷体基本没问题,手写体就看情况了。工整的手写体识别率还行,潦草的手写体基本没法看。艺术字、装饰性字体也是类似,越接近标准印刷体,识别效果越好。
如果确实需要识别特殊字体,可以考虑自己训练模型。easyocr 支持自定义识别网络,但这属于进阶玩法,需要准备标注数据、设计网络结构、训练调参,投入的时间精力不小。对于大多数应用场景,用预训练模型加上合理的预处理就够了。
7. 把 easyocr 接入实际工作流的几种思路
7.1 截图转文字:日常办公场景的自动化
我平时写东西的时候经常需要引用一些截图里的文字,手动敲太慢,用在线OCR又嫌麻烦。后来写了个小脚本,监听剪贴板,一旦发现新图片就自动识别并把文字写回剪贴板。这样截图之后直接粘贴就是文字,效率提升很明显。
实现思路是用Pillow的ImageGrab模块监听剪贴板,或者用pyperclip配合系统剪贴板 API。识别部分就是调用 easyocr,把结果拼接成字符串再写回剪贴板。这个脚本我用了大半年,稳定性不错,唯一需要注意的是识别大图的时候会有几秒延迟,习惯就好。
7.2 文档数字化:扫描件批量转可编辑文本
扫描件数字化的需求很常见,比如把纸质合同、发票、报表转成可搜索的 PDF 或者纯文本。这个场景的特点是图片数量多、格式统一、对准确率要求高。
我的做法是分三步走:预处理、批量识别、后处理。预处理包括去噪、纠偏、二值化,用 OpenCV 就能搞定。批量识别用前面讲的循环加异常捕获。后处理包括拼写检查、格式整理、关键信息提取。
如果扫描件是表格,识别出来的文字还需要按行列对齐。这个用 easyocr 的边界框坐标可以做,根据 y 坐标判断行,根据 x 坐标判断列,然后重组表格结构。实现起来有点繁琐,但逻辑是清晰的。
7.3 和 PDF 处理库配合:从 PDF 到结构化数据
PDF 文件分两种:文本型 PDF和扫描型 PDF。文本型 PDF 可以直接用pdfplumber或者PyPDF2提取文字,不需要 OCR。扫描型 PDF 本质上是一堆图片,需要先转成图片再识别。
处理扫描型 PDF 的流程是:用pdf2image把每页转成图片,然后用 easyocr 识别,最后把结果按页组织。pdf2image依赖poppler,在 Linux 上需要先安装poppler-utils,在 Windows 上需要下载 poppler 的二进制包并配置环境变量。
from pdf2image import convert_from_path import easyocr reader = easyocr.Reader(['ch_sim', 'en'], verbose=False) pages = convert_from_path('document.pdf', dpi=300) all_text = [] for i, page in enumerate(pages): result = reader.readtext(page) page_text = '\n'.join([item[1] for item in result]) all_text.append(f'--- 第 {i+1} 页 ---\n{page_text}') with open('output.txt', 'w', encoding='utf-8') as f: f.write('\n\n'.join(all_text))这里dpi=300是个经验值,太低会模糊,太高会慢且占内存。300 对于大多数文档来说够用了。
8. 关于 easyocr 的一些个人体会
用 easyocr 这段时间,最大的感受是它把 OCR 的门槛降到了很低。以前搞 OCR 要折腾环境、调参数、处理各种格式兼容问题,现在几行代码就能跑起来,而且效果还不差。对于个人项目和小型应用来说,这个性价比非常高。
当然它也不是万能的。如果你的场景对准确率要求极高,比如金融票据识别、法律文书处理,那可能还是需要商业 API 或者自己训练模型。但对于日常的截图转文字、文档数字化、图片信息提取这些需求,easyocr 完全够用。
还有一个我觉得很实用的点:easyocr 的返回结果包含置信度和边界框,这意味着你可以根据置信度做过滤,只保留高置信度的结果,或者根据边界框做版面分析。这个灵活性是很多在线 OCR 服务不具备的。
最后分享一个小技巧:如果你发现某个语言的识别效果不理想,可以试试把图片放大两倍再识别。easyocr 内部会做尺寸调整,但有时候原图太小,放大之后反而能提升检测和识别的准确率。这个技巧在处理小字号的截图时特别管用。