☰
PaddleOCR离线exe打包:模型封装、瘦身与交付自检
2026/10/24 2:33:32 网站建设 项目流程

简介:面向无Python运行环境但需要本地文字识别的用户,这份PaddleOCR打包exe离线工具资料提供了一套可参考的工程化材料,重点解决图片路径输入、OCR识别与结果写入txt的离线使用问题。rar压缩包共2000个文件,以319个py与319个pyc脚本、232个pyd和139个dll运行库为主,并包含enc模型文件、tcl/tk运行组件、txt配置说明及少量图片资源,整体约279.22MB,覆盖离线运行所需的脚本、依赖库和模型文件。已有3789人学习下载。读者可从中了解PaddleOCR API调用、PyInstaller单文件打包、模型与依赖组织方式,以及错误处理、批量识别和结果落盘等扩展思路,适合希望快速搭建本地OCR工具、降低无Python环境使用门槛的开发者参考。

1. 一台断网的工控机,把 PaddleOCR 逼成了 exe

上周去某公司现场,对方指着工位上那台只开了内网策略的工控机问:这台机器能不能扫票据?机器上没装 Python,没有 pip,连pip install paddleocr这几个字都敲不出来,唯一能用的是 U 盘和双击运行。这种场景下,paddleocr 打包 exe 离线工具才真正有了用武之地:把检测、方向分类、识别三套权重,连同 Paddle 的推理引擎和 Python 运行时,一起封进一个目录,插上 U 盘就能跑。

这份资源解决的是“环境不自由”的问题,不是识别精度问题。适合三类人:要把 OCR 交付到客户内网现场的实施工程师、需要给非技术同事做一个双击即用小工具的后端开发,以及手头有一批扫描件但不能上传到任何在线服务的从业者。它的边界也很清楚——打包出来的是推理侧能力,训练、微调、标注这些事仍然要在有环境的开发机上完成。

2. 拆开 PaddleOCR 的推理链路:离线包里到底要凑齐哪几样东西

在动手打包之前,必须先知道 PaddleOCR 一次识别到底依赖了什么。没搞清楚这一点就直接pyinstaller -F,最后大概率是拿到一个双击闪退的几十兆空壳。

2.1 一次识别经过的四道工序

输入一张图,PaddleOCR 内部走的是一条固定流水线。第一步是文本检测,DB 检测网络把图片缩放到 32 的整数倍,输出一张概率图,再做二值化和轮廓提取,得到一堆不规则的四边形文本框;第二步是透视变换,把每个斜着的框通过仿射变换拉成水平矩形;第三步是方向分类,用一个很小的分类网络判断这个框是不是倒的,是倒的就转 180 度;第四步是文本识别,CRNN 结构输出序列,CTC 解码后按字典映射成字符。

关键点在于:检测、分类、识别是三张独立的推理图,各自有一个模型目录,各自读各自的inference.pdmodel和inference.pdiparams。识别这一路还额外依赖一个字典文件,中文模型对应的是ppocr_keys_v1.txt,里面一行一个字符,CTC 解码出来的索引靠它翻译成汉字。字典丢了不会报错,只会安静地输出一串空字符串或者乱码,这个坑后面还会遇到。

2.2 离线包必须凑齐的四类东西

我把一个能跑的离线目录拆开数过,缺任何一类都跑不起来:

类别具体内容缺失后的典型症状
推理引擎libpaddle.pyd、mklml.dll、libiomp5md.dll等ImportError: DLL load failed,进程直接退出
模型权重det、rec、cls 三个目录下的inference.pdmodel/inference.pdiparams联网时悄悄去下载,断网就是连接超时
字典文件ppocr_keys_v1.txt及包内自带的 yml 配置识别结果为空串或乱码,但流程不报错
Python 层依赖numpy、opencv、shapely、pyclipper、scipyModuleNotFoundError: No module named 'shapely'

这里最容易被低估的是第四类。shapely和pyclipper是检测后处理里做多边形运算用的,很多人本地开发环境早就装好了,打包时忘了hiddenimports,结果在干净机器上一跑就缺模块。血泪经验是:打包机的虚拟环境一定要新建,只装必须的包,这样缺什么一眼就能看出来。

2.3 引擎选型:Paddle Inference 还是 ONNX Runtime

这是这套方案里唯一需要提前决策的分叉口,两条路都能出 exe,但代价完全不同。

维度Paddle InferenceONNX Runtime
产出目录体积(det+rec)约 180–260 MB约 30–60 MB
冷启动耗时(机械硬盘)1.5–3 s0.6–1.2 s
额外转换成本无,直接用官方权重需要 paddle2onnx 逐模型转,后处理参数要重调
精度表现基准基本对齐,个别长文本行会有 1–2 个字的差异
硬件加速CPU / GPUCPU / GPU / DirectML

我一般会这样选:如果交付目标是内网服务器或者性能尚可的工控机,直接走 Paddle Inference,省掉转换环节,出问题的概率最低;如果目标是老旧笔记本、要拷进 U 盘分发、或者对启动速度敏感,那就花半天时间转 ONNX,体积和启动都能砍掉一大半。新手先跑通 Paddle Inference 这条路,等打包流程稳定了再去折腾转换,否则两件事同时出问题,排查起来会非常难受。

2.4 先把权重从缓存目录里“捞”出来

PaddleOCR 默认行为是首次运行自动下载模型到用户目录下的.paddleocr缓存里,这个行为在离线环境是致命的。正确做法是在一台联网机器上先跑一次,然后把缓存目录整个搬到工程里。

import os import shutil from paddleocr import PaddleOCR # 联网机器上执行一次,触发模型下载(首次会比较慢) ocr = PaddleOCR(use_angle_cls=True, lang="ch", show_log=False) # 默认缓存位置:Windows 在 C:\Users\<用户名>\.paddleocr cache_dir = os.path.expanduser("~/.paddleocr") target_dir = os.path.join(os.getcwd(), "models") # 整目录拷贝,保留 whl/ 下的子目录结构,别只拷单个 pdmodel shutil.copytree(cache_dir, target_dir, dirs_exist_ok=True) for root, dirs, files in os.walk(target_dir): for f in files: print(os.path.join(root, f))

这段脚本干的事很简单:用一次联网运行把权重“骗”到本地,再整体复制进工程目录。执行完你应该能看到models/whl/det/ch/、models/whl/rec/ch/、models/whl/cls/这三棵子树,每棵下面都是inference.pdmodel加inference.pdiparams的组合。

参数上有两个细节要注意。lang="ch"决定下载的是中文模型,如果你要做英文或日文,这里换成对应语言代码,捞出来的目录名也会跟着变,后面写路径时别照抄。另外use_angle_cls=True才会把分类模型一起下下来,如果你确定输入图片方向都是正的,可以把它设成 False,省掉一个模型目录,体积能少十几兆。

提示:拷贝完成后,把工程里的模型目录设成只读,避免有人误删了pdiparams只留下pdmodel,那种情况下报错信息会非常含糊。

3. 用 PyInstaller 把 PaddleOCR 封成 exe:目录、入口与 spec 三件套

打包这件事,九成的失败都发生在写代码之前——目录摆错了、入口脚本没做资源路径兼容、spec 文件里漏了动态库。这一章按照可以照着抄的顺序走一遍。

3.1 工程目录怎么摆

我习惯把工程组织成这样,源文件和产物彻底分开,方便反复构建:

ocr-offline/ ├─ app.py # 唯一入口 ├─ paddleocr_offline.spec # PyInstaller 构建描述 ├─ models/ # 第 2 章捞出来的权重 │ └─ whl/ │ ├─ det/ch/ │ ├─ rec/ch/ │ └─ cls/ └─ dist/ # 构建产物,不纳入版本管理

把models放在工程根目录而不是塞进 Python 包里,是为了让 spec 里的datas写起来一目了然,同时方便后续单独替换某个模型做灰度对比。构建产物dist和中间目录build记得加进.gitignore,几百兆的二进制文件提交上去会让仓库变得很难处理。

3.2 入口脚本:资源路径兼容与参数化

PyInstaller 打包后,程序运行时的当前目录和你想象的不一样,__file__也不指向源码位置。所有对模型、字典的访问都必须经过一层路径解析,否则在开发机上跑得好好的,一打包就找不到文件。

# app.py —— 离线 exe 的唯一入口 import os import sys import json import argparse import traceback def resource_path(rel: str) -> str: """兼容源码运行与 PyInstaller 解包目录两种模式""" base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) path = os.path.join(base, rel) if not os.path.exists(path): raise FileNotFoundError(f"资源缺失,检查 spec 里的 datas: {path}") return path def flatten(result): """兼容 2.x 的嵌套列表与 3.x 的 dict 返回结构""" items = [] if not result: return items first = result[0] if isinstance(result, list) else result if isinstance(first, dict): # 3.x:字段式返回 for text, score, poly in zip(first.get("rec_texts", []), first.get("rec_scores", []), first.get("dt_polys", [])): items.append({"box": poly, "text": text, "score": round(float(score), 4)}) return items for page in result: # 2.x:[[box, (text, score)], ...] for line in page or []: box, (text, score) = line[0], line[1] items.append({"box": box, "text": text, "score": round(float(score), 4)}) return items def main(): parser = argparse.ArgumentParser(description="PaddleOCR 离线识别工具") parser.add_argument("--image", required=True, help="待识别图片路径") parser.add_argument("--out", default="", help="结果 JSON 落盘路径,留空则只打印") parser.add_argument("--use-cls", action="store_true", help="开启方向分类") parser.add_argument("--cpu-threads", type=int, default=2, help="推理线程数") parser.add_argument("--side-len", type=int, default=960, help="检测阶段长边上限") args = parser.parse_args() # 延迟导入:参数错误时不必白白加载几百兆的推理引擎 from paddleocr import PaddleOCR ocr = PaddleOCR( use_angle_cls=args.use_cls, lang="ch", det_model_dir=resource_path("models/whl/det/ch"), rec_model_dir=resource_path("models/whl/rec/ch"), cls_model_dir=resource_path("models/whl/cls"), det_limit_side_len=args.side_len, cpu_threads=args.cpu_threads, show_log=False, ) payload = json.dumps( {"image": args.image, "items": flatten(ocr.ocr(args.image, cls=args.use_cls))}, ensure_ascii=False, indent=2, ) print(payload) if args.out: with open(args.out, "w", encoding="utf-8") as f: f.write(payload) if __name__ == "__main__": try: main() except Exception: # 关掉控制台后异常会被吞掉,先落文件再说 with open("error.log", "a", encoding="utf-8") as f: f.write(traceback.format_exc() + "\n") raise

逻辑上有三处值得停一下。resource_path里的_MEIPASS是 PyInstaller 解包后的临时根目录,onedir 模式下它就是产物目录本身,onefile 模式下是%TEMP%下的随机目录,所以不要硬编码任何相对路径。flatten是为了抹平 PaddleOCR 2.x 和 3.x 的返回结构差异,同一份入口脚本能同时兼容两个版本,交付时省心不少。最外层的try/except则是给后面的--noconsole兜底,没有它,关掉黑窗口之后你只能看到一个“程序闪一下就没了”。

参数方面,--cpu-threads默认给 2 是有原因的,Paddle 默认会拿满物理核心,在一些只有 2 核的工控机上反而会因为线程争抢把整机拖死;--side-len控制检测阶段的长边缩放上限,票据这类小字密集的图给 960 到 1280 比较合适,给太小远处的字就检测不到了。

3.3 spec 文件:把 datas、binaries、hiddenimports 一次写清

命令行那一串--add-data参数一旦多起来就没法维护,我强烈建议直接写 spec。下面这份是我在多个项目里改出来的版本,核心是把 Paddle 的动态库和 PaddleOCR 的配置文件都收全。

# paddleocr_offline.spec # -*- mode: python ; coding: utf-8 -*- from PyInstaller.utils.hooks import ( collect_dynamic_libs, collect_data_files, collect_submodules ) # 权重整目录拷进去,目标路径与 app.py 里 resource_path 的相对路径保持一致 datas = [("models", "models")] datas += collect_data_files("paddleocr") # 包内 yml 配置 datas += collect_data_files("paddle") # paddle 自带的少量配置 binaries = collect_dynamic_libs("paddle") # libpaddle.pyd / mkl 系列 dll binaries += collect_dynamic_libs("paddleocr") hiddenimports = collect_submodules("paddleocr") + [ "paddle", "paddle.fluid", "shapely", "shapely.geometry", "pyclipper", "scipy.special", "scipy.linalg", "cv2", "numpy", ] a = Analysis( ["app.py"], pathex=[], binaries=binaries, datas=datas, hiddenimports=hiddenimports, hookspath=[], runtime_hooks=[], excludes=["matplotlib", "IPython", "notebook", "tkinter", "pytest"], noarchive=False, ) pyz = PYZ(a.pure, a.zipped_data) exe = EXE( pyz, a.scripts, [], exclude_binaries=True, # onedir 模式,依赖放到 COLLECT 里 name="ocr-offline", debug=False, strip=False, upx=False, # UPX 压缩会让部分 dll 加载失败,别开 console=True, # 首次构建务必 True,方便看 traceback ) coll = COLLECT( exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=False, name="ocr-offline", )

几个关键取舍。exclude_binaries=True加COLLECT是 onedir 模式,产出是一个目录而不是单个 exe,启动时不需要把几百兆解压到临时目录,冷启动能快两秒以上,交付时打个压缩包给客户解压即可。upx=False是必须的,UPX 对 Paddle 的 dll 做过压缩后,一部分机器上会报DLL load failed,这个坑很隐蔽,因为在你自己的开发机上可能完全正常。console=True先别急着关,等所有问题排查完,最后一版再改成False。

hiddenimports里的scipy.special和scipy.linalg是显式加进去的,因为 Paddle 在某些算子实现里会动态 import 这两个子模块,PyInstaller 的静态分析扫不到。

3.4 构建与首次运行验证

# 建议在专用的干净虚拟环境里构建 python -m venv .venv-build .venv-build\Scripts\activate pip install "paddlepaddle==2.6.*" "paddleocr==2.7.*" pyinstaller pip install shapely pyclipper opencv-python-headless # 清理旧产物后重新构建,spec 有改动一定要 --clean python -m PyInstaller --clean --noconfirm paddleocr_offline.spec

构建完成后,产物在dist/ocr-offline/下。验证别只在开发机上跑,那样等于没验证。我一般的顺序是:先在本机命令行跑一张图,确认能出 JSON;然后把整个目录拷到一个没有装过 Python的机器上,断网再跑一次;最后才把console改成False重新构建,再测一次闪退情况。这三步走完,交付出去的翻车概率会低很多。

dist\ocr-offline\ocr-offline.exe --image D:\samples\bill_001.jpg --use-cls --out D:\samples\bill_001.json

这条命令的含义:--image指定单张图,--use-cls打开方向分类(票据经常是横着扫进去的),--out让结果同时落一份 JSON 到磁盘,方便后续和业务系统对接。第一次运行如果超过十秒还没出结果,先去看同目录下有没有生成error.log,那里通常会有比控制台更完整的堆栈。

4. 打包 PaddleOCR 的五个高频翻车现场:现象、原因与解法

这一章全部来自实际踩过的坑,按“看到什么 → 为什么 → 怎么办”写。打包类问题最难的地方在于报错信息经常被吞掉,所以排查的第一步永远是把console打开、把异常写进日志。

4.1 双击闪退,什么提示都没有

现象:exe 复制到目标机器,双击之后黑窗口一闪而过,事件查看器里也没有有价值的记录。

原因:console=False把标准错误流重定向到了NUL,Python 抛出的异常无处可去。再加上入口脚本没有兜底的异常捕获,进程就这么静默退出了。

解决:分两步。先把 spec 里的console改回True重新构建,看真实报错;同时在app.py的__main__外面套try/except,把traceback.format_exc()追加写入同目录的error.log。这两件事做完,九成的闪退都能定位到具体是哪一行。

4.2 报No such file or directory: 'ppocr_keys_v1.txt'

现象:程序能启动,模型也加载了,但一开始识别就抛文件不存在,路径看起来是个相对路径。

原因:字典文件由paddleocr包内部通过相对路径读取,PyInstaller 只打了.py文件,没把包里的.txt资源带进去。

解决:spec 里补collect_data_files("paddleocr"),如果还不够,就手动加一条datas += [("path/to/ppocr_keys_v1.txt", "paddleocr/ppocr/utils")],目标路径必须和包源码里的相对位置完全一致。判断目标路径的办法很简单:去虚拟环境的site-packages/paddleocr/ppocr/utils/下看一眼文件在哪,就照那个结构摆。

4.3ImportError: DLL load failed while importing libpaddle

现象:在开发机上一切正常,换到目标机就报动态库加载失败,重装 VC 运行库也没用。

原因:两种可能。一是collect_dynamic_libs("paddle")没生效,mklml.dll、libiomp5md.dll这些没进_internal目录;二是目标机缺 MSVC 运行库,Paddle 的 pyd 依赖vcruntime140.dll。

解决:先检查产物目录里有没有那几个 dll,没有就在 spec 的binaries里显式加。同时给目标机装上微软的 VC++ 运行库合集,这一步在交付前就该写进实施手册,别等现场才发现。另外确认upx=False,UPX 压缩过的 dll 在很多机器上加载会失败。

4.4 处理大图时进程直接消失,没有 traceback

现象:小图正常,一遇到 A4 幅面 300dpi 的扫描件,进程无提示退出,日志里什么都没有。

原因:Paddle 默认按物理核心数开线程,在 2 核 4G 的工控机上,多线程加上大图的内存峰值很容易触发 OOM,被系统直接杀掉,这种情况下 Python 层根本来不及捕获。

解决:把cpu_threads压到 2 甚至 1,把det_limit_side_len降到 960,再用 OpenCV 在送进模型前做一次长边预缩放。如果是批量场景,改成一张一个子进程,让父进程能感知到子进程的退出码,而不是整个程序一起死。

4.5 打包后识别结果和开发机不一致

现象:同一张图,开发环境识别 20 行全对,exe 里只出来 18 行,或者文字内容有几个字不一样。

原因:模型目录被污染了。很常见的情况是models/whl/det/ch里同时躺着新旧两版权重,或者只换了 rec 模型却忘了同步换字典,导致索引映射错位。

解决:在入口脚本启动时打印一份模型指纹,把每个inference.pdiparams的文件大小和 MD5 前八位输出到日志,交付前后各比对一次。这件事看着繁琐,但它是唯一能证明“客户手上跑的就是我验过的那版模型”的手段,我后来把它做成了构建脚本里的固定一步。

5. 把 260MB 压到 60MB:模型裁剪、onedir 取舍与 ONNX 转换

能跑起来只是及格线。真正交付的时候,客户会问你为什么一个 OCR 工具比他们的办公软件还大,为什么双击之后要等三秒才出界面。这一章讲的就是怎么把体积和启动时间压下来,同时不把精度搞崩。

5.1 先做减法:砍掉分类模型,换移动端权重

最省事的两刀。第一刀砍方向分类,如果你的输入源是平板扫描仪或者固定的高拍仪,图片方向本来就是正的,--use-cls关掉,直接把cls_model_dir从 spec 的 datas 里去掉,体积少十几兆,每张图还少一次前向计算。第二刀换移动端权重,PaddleOCR 官方提供的 mobile 系列模型参数量只有 server 版的零头,在票据、证件这类版式规整的场景上,精度差距通常在 1 个百分点以内。

组合产出目录体积单张票据耗时(2 核)
server det + server rec + cls约 260 MB1.8 s
server det + mobile rec约 190 MB1.1 s
mobile det + mobile rec约 120 MB0.7 s
mobile det + mobile rec(转 ONNX)约 55 MB0.5 s

表里的数字是在同一台机器上构建后实测的近似值,会随 PyInstaller 版本和依赖变动,看趋势不看绝对值。我的建议是从第二行开始试,如果某批票据的识别率掉了,再把 rec 换回 server 版,det 保持 mobile——检测阶段对精度的敏感度通常低于识别阶段。

5.2 onedir 和 onefile 的取舍

很多人默认用-F(onefile),觉得单文件更“干净”。在 PaddleOCR 这个场景下我明确不建议。onefile 的本质是把所有东西压缩进一个 exe,每次启动都要解压到%TEMP%下的临时目录,260MB 的包解压一次就是两三秒,而且临时目录在某些受管策略的机器上会被清理或限制写入,直接导致启动失败。

改法很简单,spec 里保留exclude_binaries=True和COLLECT,产出是一个文件夹。交付时打成 zip,写一句“解压到任意目录后双击ocr-offline.exe”。这样冷启动能压到 1 秒出头,而且出问题时可以直接进目录看_internal里到底缺了哪个 dll,排查成本低一个量级。

5.3 转 ONNX:用 paddle2onnx 换掉整个推理引擎

体积的大头其实不是模型,是 Paddle 的推理引擎和它带的 MKL 数学库。把模型转成 ONNX,用 onnxruntime 跑,等于把这一大坨换成一个几十兆的运行时。转换命令不复杂,难点在后处理参数对齐。

# 三个模型逐个转,opset 11 兼容性最好 paddle2onnx --model_dir models/whl/det/ch \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file models_onnx/det.onnx \ --opset_version 11 \ --enable_onnx_checker True paddle2onnx --model_dir models/whl/rec/ch \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file models_onnx/rec.onnx \ --opset_version 11 \ --enable_onnx_checker True

参数说明:--model_dir指向第 2 章捞出来的目录,注意必须是同时含pdmodel和pdiparams的那个层级;--opset_version 11是我试过的兼容性最好的版本,用更新的 opset 有时会在某些算子上报不支持;--enable_onnx_checker True会让工具转完之后自检一遍图结构,有问题当场就报,别省这一步。

转换完成后要用同一批图做一次对拍:把 ONNX 版的输出和 Paddle 版的输出逐条比对,重点看文本框数量和后处理参数——det_db_thresh、det_db_box_thresh、det_db_unclip_ratio这几个值在两条链路上的默认行为可能不一致,需要手动对齐。对拍不上就直接上线,是这类改造最常见的翻车方式,表面上跑通了,实际召回率悄悄掉了几个点。

5.4 删动态库的边界在哪

有人会想进一步瘦身,跑去_internal目录里手动删 dll。可以做,但要有边界:libpaddle.pyd同目录下以mkl、iomp、paddle开头的文件别动;cv2相关的 dll 别动;api-ms-win-*开头的系统转发库别动。可以动的是matplotlib、scipy的部分子模块、以及paddle里跟训练相关的那一堆(paddle.fluid之外的 trainer 相关 dll)。

稳妥的做法是在 spec 的excludes里排除掉打包时就确定不要的包,而不是事后手工删文件。因为手工删完之后,你手上那份产物和 spec 描述的就不再是一回事了,下次重新构建又会恢复,最终没人说得清发布的是哪个版本。

6. 让 exe 变成可集成的命令行工具:批量调用与交付自检

到这一步,工具已经能跑了,但它还只是个“双击试试看”的东西。真正交付给业务方,它得能被人调用、能批量处理、能证明自己没跑偏。

6.1 用参数把 exe 变成一个可编程组件

第 3 章的app.py里已经把输入输出都参数化了,这里补上最实用的两个扩展:一个--batch接受目录,一个--min-score过滤低置信度结果。改法是在argparse里加两个参数,然后在主流程里做分支,逻辑很直白,不再贴全量代码。

真正值得说的是被调用的姿势。业务系统集成时不要用 shell 字符串拼接路径,中文路径和空格会让你怀疑人生。用subprocess.run传列表,并显式指定编码:

# batch.py —— 放在任意一台装了 Python 的机器上,调用打好的 exe import json import subprocess from pathlib import Path EXE = Path(r"D:\deliver\ocr-offline\ocr-offline.exe") # 产物目录里的 exe ROOT = Path(r"D:\scans") for img in sorted(ROOT.rglob("*.jpg")): proc = subprocess.run( [str(EXE), "--image", str(img), "--cpu-threads", "2"], capture_output=True, text=True, encoding="utf-8", # 不指定的话中文 Windows 上是 GBK,JSON 里的中文会乱码 timeout=120, # 单张超时就杀掉,避免一张坏图卡死整批 ) if proc.returncode != 0: print("[FAIL]", img.name, proc.stderr[-300:]) continue data = json.loads(proc.stdout) kept = [it for it in data["items"] if it["score"] >= 0.85] print("[OK]", img.name, f"{len(kept)}/{len(data['items'])}")

这段脚本的三个参数值得解释。encoding="utf-8"是必须的,exe 内部print出来的是 UTF-8,父进程按 GBK 解会得到一堆问号。timeout=120是后悔药,OCR 遇到超大图或者某些畸形文件可能会挂很久,没有超时的话整个批处理就卡在那里了。score >= 0.85是置信度过滤,实际使用中低于这个值的行绝大多数是噪声或者印章干扰,直接丢掉比交给下游纠错更划算——具体阈值要按你的业务调,证件类可以放到 0.9,手写体得放到 0.6。

6.2 交付前跑一遍这张自检表

每一次打包,我都会照着下面这张表走一遍,少一步都可能在现场返工。

检查项怎么查通过标准
模型指纹一致启动日志里的 pdiparams 大小与 MD5 前八位与构建机记录完全一致
干净机可运行拷到未装 Python 的机器,断网运行正常输出 JSON,无缺库报错
中文路径图片放在D:\测试 数据\票据 01.jpg路径含空格和中文也不报错
异常留痕故意给一个不存在的图片路径同目录生成error.log,内容可读
批量稳定性连续跑 200 张,观察内存曲线内存不持续上涨,无进程退出
结果可复现同一张图跑三次三次 JSON 完全一致

最后一行特别重要。有些人为了“提升效果”在代码里加了随机采样或者多尺度融合,结果同一张图每次跑出来都不太一样,业务方一对账就露馅了。离线工具的第一价值是稳定可复现,其次才是精度。

6.3 一个我踩过的坑:别在最后一版才关控制台

说个具体的。有一次我在交付前一天才把console改成False重新构建,结果目标机器上一运行就闪退。排查了两个小时才发现,是关掉控制台之后,某段第三方库往stdout写日志时抛了OSError,而这个异常在console=True的时候被正常的输出缓冲掩盖了。从那以后我每次做这类打包,都会在功能验证的中期就用console=False构建一版跑一遍,而不是留到最后。

同样是那次之后,我养成了一个习惯:构建脚本固定三步——--clean重新构建、拷到干净机断网跑三张图、比对模型指纹。三步全绿才算这版可交付。听着笨,但它替我省下的现场时间,远多于这点机械劳动。

希望这些能帮你少走几趟弯路,把工具安安稳稳地交到那台断网的机器上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询