简介:基于百度开源PaddleOCR打造的本地离线识别资源包,面向需要在无网络环境下完成文字检测与识别的Python或C++开发者,既可保障数据安全,又能获得快速响应,适合对隐私敏感或网络不稳定的业务场景。资源共35个文件,压缩后约67.15MB,除mklml.dll、opencv_world450.dll、mkldnn.dll等数学计算、图像处理与深度学习推理依赖库外,其中mklml.dll负责底层数学运算,opencv_world450.dll用于图像预处理,mkldnn.dll加速神经网络推理;还包含params、model等预训练模型文件,以及可直接运行的ocr.exe和测试控制台工具。随包附带的VC++工程源码,完整演示了如何在C++环境中调用DLL接口实现识别功能,另有测试图片和可视化输出结果,方便对比验证。目前已有22859人学习下载,无论是快速搭建离线OCR能力、研究PaddleOCR跨语言调用,还是进行Windows端C++二次开发,都能从中获得参考。
1. 为什么把识别放本地:PaddleOCR离线识别的真实边界与适用场景
给一个档案室做数字化整理时,扫描仪连着的那台机器严格隔离,不能访问外网,也不允许把图片传到任何云服务。最初想用在线识别,数据安全直接被打回。后来在纯内网服务器上装了百度开源的 PaddleOCR,从图片里抽身份证号、合同条款、表格标题,一张普通扫描件两秒左右出结果,通用识别度极高,连倾斜摆放的文件也能自动转正识别。这里想先给个结论:本地离线识别的难点从来不在识别率,而在环境依赖和工程封装。适合做文档系统、内网隔离环境数据录入的团队参考,也适合想摆脱按量付费、把OCR能力握在自己手里的个人开发者。
2. 离线部署前的三件事:模型选型、运行环境与首条识别命令
2.1 三段式结构:检测、方向分类、识别各管一件事
用 PaddleOCR 做离线识别前,先得理解它的管线并不只有一个“识别模型”。我调试时习惯把一次识别拆成三段:检测(Detection)、方向分类(Direction Classification)、识别(Recognition)。检测负责把图片里每一行文字的位置框出来,方向分类负责判断这张图整体有没有旋转 90 度或 180 度,识别模型才真正把框出来的文字区域翻译成字符串。这样拆开的好处是,任何一段出问题都能单独排查,而不是对着最终结果瞎改参数。
选型上,官方发布的 PP-OCRv4 系列是目前离线项目里最稳的基准。它分 mobile 和 server 两种规格:mobile 模型体积小、CPU 上速度快,离线服务器多数不给 GPU,我一般直接用 mobile;server 模型精度略高但显存占用也高,只有显卡机器才会选。方向分类模型默认开启,它对办公文档这类白底黑字的图几乎无感,但如果你的图片来自手机拍摄,经常会歪斜,开着它能救回不少识别率。
另一个容易忽略的是语言参数。lang='ch'表示简体中文加英文,官方还有en、japan、korean等预训练模型。如果你只识别英文,用lang='en'比ch更准,因为中文模型会在中英混排上做折中。
离线部署还需要区分推理模型和训练模型。PaddleOCR 模型库里带_infer后缀的是推理模型,也就是部署时真正要加载的文件;带_train或直接命名成_rec的用于二次训练。有些人把训练模型塞进det_model_dir,初始化不报错,但识别结果明显紊乱,就是文件选错了。这一点放到第五章再展开。
2.2 环境准备:CPU机器也能跑,但版本组合要锁死
先说 Python 版本。PaddleOCR 官方长期支持的是 3.8 到 3.10,3.11 以后容易遇到 opencv-python 和个别本地库的兼容问题。离线生产环境我建议用 conda 单独建一个环境,不要碰系统自带的 Python,尤其是那些被深度定制过的内网机器,一旦 system Python 的依赖被覆盖,整台服务器可能连 ssh 登录都会受影响。
# 用 conda 隔离依赖,避免污染系统 Python conda create -n ocr python=3.9 -y conda activate ocr # 安装 CPU 版 PaddlePaddle,国内网络用百度镜像更快 pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple # 安装 PaddleOCR pip install paddleocr -i https://mirror.baidu.com/pypi/simple装完先别急着跑识别,先确认核心包能不能导入:
python -c "import paddle, paddleocr; print(paddle.__version__, paddleocr.__version__)"如果部署机器完全离线,做法是在联网机器上把所有依赖打包成 wheelhouse,再整体拷贝过去。这里不能只下主包,PaddleOCR 会拉动 opencv-python、shapely、pyclipper 等一堆带二进制依赖的包,缺一个就 import 失败。
# 联网机器上执行 pip download paddlepaddle paddleocr -d ./offline_packages # 拷贝到离线机器后执行 pip install --no-index --find-links=./offline_packages paddlepaddle paddleocr版本锁定的意义在于:PaddleOCR 的 API 在几个大版本里改过多次,use_angle_cls改成cls、ocr()改成predict()都是真实发生过的变化。我一般会在安装成功后立刻执行pip freeze > requirements.txt,把这个文件一起带到离线机器,避免“今天能跑,明天换包后跑不起来”的尴尬。
2.3 首跑验证:一张图跑通端到端的离线识别
环境就绪后,第一件事不是写服务,而是先用一段最简脚本验证整条链路。下面这段代码是离线识别的最小可运行版本:
# 首跑验证脚本:注意模型会在第一次运行时自动下载 from paddleocr import PaddleOCR ocr = PaddleOCR( use_angle_cls=True, # 开启方向分类,对乱序拍摄的图片更友好 lang='ch', # 中文简体 + 英文 use_gpu=False, # 无 GPU 环境保持 False show_log=False, # 关闭初始化日志,避免刷屏 enable_mkldnn=True # CPU 推理加速,官方编译版本默认支持 ) result = ocr.ocr('test.png', cls=True) # 返回结果的结构是 list,每个元素对应一个检测到的文本框 for line in result: # line[0] 是四角坐标,line[1] 是 (识别文本, 置信度) print(line[1][0], line[1][1])这里最容易困惑的是ocr.ocr()返回的三层嵌套结构。result是列表,列表每一项等于一个文本行;每个文本行又是一个二元组,第一项是矩形四点坐标,第二项才是文本和置信度。新手常常直接print(result)得到一大串数组,误以为识别失败了。实际只要line[1][0]是你想要的字符串。
还有两个小细节。第一,use_angle_cls=True是模型层面开启方向分类,但运行时还需要传入cls=True,两者缺一不可;第二,如果是完全离线的机器,第一次执行会卡在模型下载,所以我通常先在联网机器上跑一次,等模型自动缓存到~/.paddleocr,再把整个目录拷到离线机器上,具体操作在避坑章里写。
一个正常现象是:第一次初始化要加载检测、方向分类、识别三套模型,可能花三五秒,之后单张推理就快了。如果你的业务图片区域固定,比如只识别发票左上角的发票代码,可以先裁剪出那块区域再调用ocr.ocr(),比让检测模型在整张图上跑更快、更准。
3. 把离线识别封装成本地服务:接口设计、并发与内存控制
3.1 用 FastAPI 做一个同步可用的识别接口
脚本验证过后,下一步自然是把离线识别变成可以被业务系统调用的服务。PaddleOCR 本身是 Python 库,我见过很多人直接把它塞进 Django/FastAPI 接口里,结果一压测就内存溢出。常见做法是让识别引擎常驻内存,每一个请求复用同一个实例,绝不能在请求函数里反复初始化 OCR 对象。
from fastapi import FastAPI, UploadFile from paddleocr import PaddleOCR import os app = FastAPI() # 模块加载时初始化一次,全局复用 ocr = PaddleOCR( use_angle_cls=True, lang='ch', use_gpu=False, show_log=False, enable_mkldnn=True ) UPLOAD_DIR = "/tmp/ocr_files" os.makedirs(UPLOAD_DIR, exist_ok=True) @app.post("/ocr") async def do_ocr(file: UploadFile): # 先做类型检查,避免非图片文件进入识别流程 if not file.content_type.startswith("image/"): return {"code": 400, "msg": "not image"} # 先落盘再识别,PaddleOCR 的输入接口接受文件路径或 ndarray filename = os.path.join(UPLOAD_DIR, file.filename) content = await file.read() with open(filename, "wb") as f: f.write(content) result = ocr.ocr(filename, cls=True) lines = [] for line in result: text, score = line[1] lines.append({"text": text, "score": float(score)}) return {"code": 0, "data": lines}为什么先落盘?因为 PaddleOCR 的输入可以传图片路径,也可以传 ndarray,但直接传await file.read()得到的 bytes 是不行的。先把文件存临时目录再识别,虽然多了一次磁盘读写,但代码最简单,出问题时也方便直接拿原图排查。要追求性能,可以用cv2.imdecode把 bytes 转成 ndarray 再传,前端需要约定图片编码格式,这里不展开。
注意一个关键点:async def只是让 FastAPI 能同时接收请求,真正的推理仍是串行的。CPU 推理受 GIL 限制,多线程不会让识别并行。要真的并行,必须走下一节的多进程方案。
3.2 批量识别任务:多进程与识别串行化
当业务变成批量扫描几百页文件时,单实例再快也扛不住。此时不要开 100 个线程,线程池在 GIL 下反而会让每个请求都变慢。我一般用multiprocessing创建 4 个 worker,每个 worker 独立加载一套 PaddleOCR 模型,任务由主进程分发。
from multiprocessing import Pool from paddleocr import PaddleOCR def worker_init(): # 每个进程初始化一次,将识别引擎放到全局变量里 global ocr ocr = PaddleOCR( use_angle_cls=True, lang='ch', use_gpu=False, show_log=False, enable_mkldnn=True ) def recognize(image_path): # 返回值需要是可序列化的普通类型 result = ocr.ocr(image_path, cls=True) output = [] for line in result: text, score = line[1] output.append({"text": text, "score": float(score)}) return output if __name__ == "__main__": image_paths = ["page1.png", "page2.png", "page3.png", "page4.png"] with Pool(processes=4, initializer=worker_init) as pool: results = pool.map(recognize, image_paths) # results 与 image_paths 顺序一一对应 for path, lines in zip(image_paths, results): print(path, lines)这里processes=4不是随便定的。每个 worker 常驻约 300 到 500 MB 内存,因为要同时加载检测、方向分类、识别三套模型,8 GB 内存的机器开 4 个已经是保守值;如果图片分辨率较高,预处理和推理时的临时内存还会翻倍。建议先在单进程任务里用htop观察内存峰值,再反推并发数,而不是拍脑袋填 8。
另一个重点是Pool.map会把整个列表一次性塞进去。如果图片有几千张,主进程内存容易被待处理路径占满。更稳的做法是使用pool.imap(recognize, image_paths, chunksize=10)做惰性迭代,处理一张吐一张,内存占用是常数级。这个区别在大批量任务里非常明显。
3.3 长时间运行的内存回收
服务或任务跑一个小时后,常见现象是识别速度越来越慢、内存涨了 30% 不回头。单独抽一张图来看,模型本身不泄漏,泄漏点通常在图片解码、opencv 的缓存,以及 PaddleOCR 内部累积的中间张量。与其手动找泄漏,不如让 worker 定期退役。
with Pool( processes=4, initializer=worker_init, maxtasksperchild=50 # 每个进程最多处理 50 个任务后自动回收 ) as pool: pool.imap(recognize, image_paths)maxtasksperchild=50是一剂后悔药:worker 处理完 50 张图后进程被销毁,内存完全还给系统,然后主进程重建一个干净 worker。代价是重启后要重新加载模型,多花两三秒,但能保证服务长期稳定。这个参数适合批量任务;如果用于实时接口,要接受每 50 次请求出现一次额外延迟,通常不建议。
还有一个隐性坑:GPU 环境下进程销毁后显存不一定立刻释放,还会伴随 CUDA context 重建,maxtasksperchild要慎用。CPU 离线部署没有这个问题,可以放心。
4. 换业务场景换配置:识别参数、模型与预处理调优
4.1 必调参数:det_limit_side_len、det_db_thresh、rec_batch_num
“识别度极高”的口碑不是默认参数白给的,默认参数只是对不同图片有很强的泛化能力。你希望在一类业务图片上做到极致,这几个参数值得先调。
| 参数 | 默认值 | 影响 | 调整经验 |
|---|---|---|---|
| det_limit_side_len | 960 | 检测网络输入的短边/长边限制,过小会截断大段文本 | 长截图调 1280 到 1920,代价是检测变慢 |
| det_db_thresh | 0.3 | 检测响应阈值,越低越容易把模糊背景框进来 | 低对比度图片降到 0.2,高噪声图提高到 0.4 |
| det_db_box_thresh | 0.6 | 文本框过滤阈值,控制输出框完整度 | 漏框时降到 0.5,多框时升到 0.7 |
| rec_batch_num | 6 | 识别阶段一次送入多少行文本 | 小图多行时提高能提速,但内存占用线性增加 |
| cls_thresh | 0.9 | 方向分类判定阈值 | 旋转图多降到 0.7,但会产生误判 |
这些参数在初始化时传入即可:
ocr = PaddleOCR( det_limit_side_len=1280, det_db_thresh=0.25, det_db_box_thresh=0.5, rec_batch_num=4, use_angle_cls=True, lang='ch', use_gpu=False )参数命名的规律是:det_前缀给文本检测模型,rec_前缀给识别模型,cls_给方向分类。改完参数必须重新计时,因为提高det_limit_side_len之后检测时间可能翻倍,在速度敏感的离线任务里不划算。
这里要强调调参纪律:一次只动一个参数,固定二十张覆盖业务典型情况的测试图,记录漏框、多框、错字数量。不要凭感觉同时调三个参数,否则出了问题根本不知道是谁的锅。
4.2 高分辨率图片与长图:先切图再识别
高分辨率扫描件常见 300 dpi 下的 A4 图会到 2500×3500 像素。直接丢给 PaddleOCR 时,det_limit_side_len默认会将图像缩放,结果小字直接糊掉。常见做法是保持参数不变,先做切片:用 OpenCV 把大图按重叠 10% 切成小块,分别识别后再按坐标拼回去。
import cv2 def split_image(image_path, size=960, overlap=0.1): img = cv2.imread(image_path) h, w = img.shape[:2] step = int(size * (1 - overlap)) tiles = [] for y in range(0, h, step): for x in range(0, w, step): tile = img[y:y + size, x:x + size] tiles.append(((x, y), tile)) return tiles坐标处理时要留意:每个 tile 识别出来的文本框坐标是相对 tile 自身的,要映射回原图必须加上 tile 左上角坐标。重叠区域会出现同一行文字被识别两次的情况,业务侧需要去重:按识别文本和坐标区域做合并,通常重叠行文本相同,直接保留置信度高的一条。
一条血泪经验:切片后不要轻易再对 tile 做缩放。有些同学担心小图识别率低,把 tile 放大了两倍再识别,结果速度变慢,识别率并没有变好,反而让笔画变形。PaddleOCR 内置的检测网络对 960 左右的输入已经做过充分训练,强行放大只会破坏特征分布。
4.3 低配置机器上提速:tiny 模型与推理后端
很多离线部署的机器只是 2 核 4G 的工控机,跑标准模型确实吃力。PaddleOCR 官方一直维护着一组更轻量的识别模型,命名里常带 tiny 字样,专门为资源受限场景准备。通用识别度会弱一点,但换来的是加载时间和单张耗时的大幅下降。如果你的业务是简单的数字、英文单据,完全可以只换识别模型,检测模型继续用 mobile,组合起来跑。
# 假设你已经在离线目录放好了模型文件 ocr = PaddleOCR( det_model_dir='./model/ch_PP-OCRv4_det_infer/', rec_model_dir='./model/ch_PP-OCRv4_rec_tiny_infer/', # tiny 识别模型 use_angle_cls=True, lang='ch', use_gpu=False )模型文件的目录名和版本必须对应,不能把不同代际的 det 和 rec 混用,否则识别结果会出现大量错字。另外 CPU 推理时打开enable_mkldnn=True和cpu_threads=4两个选项,四核机器上提速明显。cpu_threads过大会引起线程争抢,我一般直接设成物理核数,在 2 核机器上就设 2。
如果你在官方模型库找 tiny 模型,主要看识别模型,检测模型一般直接用 mobile 即可。方向分类模型体积很小,对整体速度影响有限,不建议为了省那几毫秒关闭,否则歪斜图片会让识别率明显下降。
5. 避坑:离线识别环境里最常见的五个翻车现场
5.1 离线安装依赖漏包
现象:离线机器上pip install paddleocr后,import paddleocr报ModuleNotFoundError: cv2或protobuf、pyclipper缺失。
原因:PaddleOCR 是个库,不是单文件,安装时 pip 会拉一串依赖。离线安装时如果只拷贝了主包,依赖全部缺失,而且错误提示未必直接说明哪个包导致。
解决:在联网机器上用pip download一次拿全,再整体拷贝。
pip download paddlepaddle paddleocr -d ./offline_packages拷贝到离线机器后执行:
pip install --no-index --find-links=./offline_packages paddlepaddle paddleocr注意下载依赖时不要加--no-deps,否则还是缺依赖。装完后用pip list检查 opencv-python 版本,PaddleOCR 对 opencv 版本兼容性很挑剔,遇到 cv2 报错可以尝试固定到比较稳定的版本组合。
5.2 离线模型放置位置不对,初始化卡死
现象:在离线机器上首次初始化PaddleOCR(),日志停在 downloading,然后超时。
原因:PaddleOCR 默认会去远程拉模型,离线机器上没有网络,或者网络策略禁止访问外部下载地址。
解决:先在一台能联网的机器上跑一次识别,让模型自动下载到~/.paddleocr/,把整个目录拷贝到离线机器同一用户目录下;也可以用det_model_dir和rec_model_dir直接指定模型解压目录,彻底绕开下载逻辑。
# 在线机器跑一次生成缓存 python -c "from paddleocr import PaddleOCR; PaddleOCR(use_angle_cls=True, lang='ch')" # 找到模型目录并打包拷贝 find ~/.paddleocr -maxdepth 4 -type d离线机器放置时要注意路径权限:如果服务用 systemd 启动,运行用户是 www-data,模型目录必须在 www-data 的可读路径下,否则运行到一半会报权限错误。把模型目录放到/opt/ocr/models并修改属主,比放在用户 home 下更省心。
5.3 识别结果写入文件乱码
现象:命令行打印出来没问题,写入 CSV 后 Excel 打开全乱码。
原因:Windows 的 Excel 默认用 GBK 打开 CSV,PaddleOCR 返回的是 UTF-8 字符串。
解决:写入时用 UTF-8-SIG 编码。
import csv with open('result.csv', 'w', newline='', encoding='utf-8-sig') as f: writer = csv.writer(f) for text, score in results: writer.writerow([text, score])这不是 OCR 的坑,是文件编码的坑,但十个做识别任务的人里有三四个会遇到。如果还要写入数据库,记得表结构字段的排序规则也要支持中文,否则后面比对文本时会出怪问题。
5.4 GPU 装成摆设
现象:use_gpu=True不报错,nvidia-smi里看不到 Python 进程在算,速度也没提升。
原因:装的是 CPU 版 paddlepaddle,或者 GPU 版与 CUDA 版本不匹配,导致 paddle 静默回退到 CPU。
解决:安装前明确机器的 CUDA 版本,再选对应的paddlepaddle-gpu版本;初始化后必须验证。
import paddle print(paddle.is_compiled_with_cuda()) # 必须 True print(paddle.device.get_device()) # 实际使用的设备不要只看pip list里有paddlepaddle-gpu就以为是 GPU 版,很多机器上因为 GLIBC 版本冲突,GPU 包安装后被 CPU 版覆盖了。这个坑在离线环境更隐蔽,因为缺少联网搜索排错的条件。
5.5 长文本漏识别,误以为是模型不行
现象:识别一份合同,中间某几行漏掉,但单独截取那几行又可以识别。
原因:det_limit_side_len默认值限制了检测网络输入尺寸,长文本行被缩放后宽度超过了模型能力;或者det_db_box_thresh过高把低置信度的文本框滤掉了。
解决:先调大det_limit_side_len,配合前面的切图策略;如果只是偶发漏行,把det_db_thresh降到 0.2 到 0.25 再看。漏行问题八成在检测阶段,不要急着换识别模型。
这五条是多次离线项目里最容易踩的坑。表面看是模型环境问题,实际上多数是部署习惯问题:版本不锁定、依赖不打包、模型路径不固定。
6. 拿自己的数据说话:用 PaddleOCR 训练自定义识别模型的关键动作
最后说一个更大的话题:当你的场景是设备铭牌、印章编号、仪表读数这类专用字体时,PaddleOCR 的通用识别度再高也顶不住。这时候要把离线识别升级成“带本地模型定制能力”的离线识别。PaddleOCR 配了一套完整的训练链路,关键动作只有三个:标注、转格式、微调。
先标注。官方配套的 PPOCRLabel 是图形化标注工具,能直接对图片画框并转出 PaddleOCR 的训练格式。这一步没有捷径,标注质量决定模型上限。要做的不是简单拉框,而是把每行文字框准,标点符号也不能漏,错字会直接污染训练集。
转格式。标注完会得到Label.txt。训练识别模型时,建议把图片和文本整理成官方要求的目录结构,通常是train_data/rec/下放图片,train_data/rec/train_list.txt每行写图片路径和标签。注意 label 里的特殊字符要转义,不常见的繁体字不要删,否则模型没有样本可学。
微调命令。以官方仓库的识别模型配置为例,在训练机上执行:
python tools/train.py -c configs/rec/PP-OCRv4/ch_PP-OCRv4_rec.yml \ -o Global.pretrain_model=./pretrained/rec_model \ -o Global.output_dir=./output \ -o Global.epoch_num=100 \ -o Global.save_epoch_step=10 \ -o Train.dataset.data_dir=./train_data/rec \ -o Train.dataset.label_file_list=./train_data/rec/train_list.txt跑完在output/下导出推理模型,替换之前部署目录里的 rec 模型,离线服务就拥有了你的专属识别能力。整个流程下来,平时觉得难的识别项目,最后都会落回到数据质量上。
最后分享一个习惯:模型上线前,我固定准备五十张覆盖所有异常情况的业务图片,每次改配置或换模型都先跑一遍,记录漏行、错字和总耗时。离线识别最怕的是黑匣子——都说识别度高,但没人说清楚高在哪一类图上。有了固定测试集,谁再问识别率,直接发对比表,省去无休止的争论。希望帮到你。
本文还有配套的精品资源,点击获取