资讯详情

PP-OCR v4/v5 ONNX Runtime轻量部署实战与调参指南

📅 2026/10/23 6:57:52 | 华诺云谱 👁 阅读
PP-OCR v4/v5 ONNX Runtime轻量部署实战与调参指南
之前好几个项目都要上OCR识别检测倒是好说真正让人头疼的是模型从Paddle环境搬到生产环境这事。PP-OCR v4和v5的精度和速度确实能打但你不可能在每个服务上都装一套完整的Paddle全家桶更别说还要处理一堆版本依赖。我自己在几个票据识别和单据录入项目里反复折腾过Paddle Inference、ONNX Runtime、TensorRT这几条路线最后沉淀出来一套叫DeploySharp的轻量部署工具链专门解决PP-OCR v4/v5从导出到落地的整个过程。这篇文章就把DeploySharp怎么用ONNX Runtime跑PP-OCR v4/v5的完整经验拆开讲清楚包括模型导出、预处理对齐、后处理恢复、性能调参适合正在做OCR服务化或者边缘部署的开发者参考。1. 为什么把PP-OCR v4/v5部署在ONNX Runtime上1.1 PP-OCR v4/v5到底强在哪里先别急着写代码得先搞清楚我们要部署的东西是什么。PP-OCR v4和v5并不是一个单模型而是一条完整的文字识别管线通常由三部分组成文本检测模型负责定位图片里的文字区域方向分类模型负责判断每个检测出的文本块是否倒置文本识别模型负责把规整后的文本行图片转成字符串。三者的关系有点像流水线检测找到大概的位置分类把方向纠正过来识别最后出结果。v4版本用的是DBNet做检测、SVTR_LCNet做识别在速度和精度的平衡上已经做得很好。v5版本则是在训练策略、模型结构和推理效率上又优化了一轮官方给出的数据是端到端推理速度提升22%左右平均精度也有提升。对做实际项目的开发者来说v5最大的吸引力不是精度那几个点而是同样的硬件条件下能跑得更快真机上部署更划算。但问题来了PP-OCR的原始模型是PaddlePaddle框架的产物你不能让线上服务直接依赖Paddle Inference跑推理那意味着服务器上得装Paddle、装CUDA对应版本、处理一堆动态库。更麻烦的是团队里其他同事维护的时候光环境同步就能耗掉半天。所以我们才需要一层“转换封装”让模型变成通用的ONNX格式再用ONNX Runtime来推理。1.2 ONNX Runtime和DeploySharp的定位ONNX Runtime是微软开源的跨平台推理引擎相当于一个“模型运行器”。Paddle模型经过转换变成ONNX格式后就脱离了Paddle环境任何平台只要能跑ONNX Runtime就能跑这个模型。DeploySharp则是我在这套思路之上整理出的一层工具链它的作用不是替代ONNX Runtime而是把模型转换、预处理、后处理、性能调参这些重复劳动固化下来让你拿到PP-OCR v4/v5就能快速接进自己的服务。DeploySharp的核心设计哲学有两条第一不修改模型本身只做推理管线的编排第二所有预处理和后处理严格对齐PaddleOCR官方实现避免“模型能出结果但对不上”这种尴尬情况。1.3 部署方案选型Paddle Inference、ONNX Runtime、TensorRT怎么选很多新手一上来就问“哪个方案最好”其实得看场景来定。Paddle Inference的优势是跟Paddle模型原生兼容算子支持最全但环境依赖重部署到别人的服务器上容易翻车。而且如果你跑的是GPUTensorRT场景Paddle Inference的配置复杂度也不低。ONNX Runtime的优势是轻量、跨平台、支持CPU/GPU/TensorRT多种后端Python和C都有API适合做服务化部署或边缘设备部署。缺点是没有Paddle自家那么多模型层面的魔法优化但PP-OCR这种成熟模型导出成ONNX后算子都很标准性能差距很小。TensorRT是英伟达的加速方案推理速度最快适合GPU服务端高吞吐场景但工程复杂度最高需要显存、需要转引擎、动态shape还得配置profile。我现在的习惯是CPU环境直接用ONNX Runtime跑GPU环境如果QPS要求不高也先用ONNX Runtime的CUDA ExecutionProvider顶住等确定瓶颈在推理而非IO再考虑上TensorRT。DeploySharp默认就把ONNX Runtime作为第一优先级这也符合大部分项目的实际需要。2. 环境准备与模型导出细节2.1 前期环境清单工具链本身不复杂但版本要对齐不然会在转换阶段莫名其妙卡住。我用的是一套比较保守的版本组合Python 3.8到3.10之间比较稳3.11以上某些paddle2onnx版本会有兼容问题paddlepaddle 2.5以上建议2.6paddle2onnx 1.0.9以上onnxruntime 1.15以上建议1.17opencv-python负责图像处理numpy安装命令很简单但要注意从头装paddlepaddle时如果你不需要训练装CPU版本的paddlepaddle就够了几百兆省下来。GPU版本只有在需要验证某些算子导出时才有必要。2.2 导出检测、方向分类、识别三个模型从PaddleOCR官网下载推理模型时会分别拿到三个目录det模型目录、cls模型目录、rec模型目录。每个目录里都有inference.pdmodel和inference.pdiparams两个文件。导出命令的套路是一样的只是模型目录路径不同。pip install paddle2onnx paddlepaddle paddle2onnx --model_dir ./models/ch_PP-OCRv5_det_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ./onnx/ppocrv5_det.onnx \ --opset_version 12 \ --enable_onnx_checker True识别模型和方向分类模型只需要把model_dir和save_file换掉命令结构完全一样。导出完成后最好用onnx库做一次检查确认模型结构没问题import onnx model onnx.load(./onnx/ppocrv5_det.onnx) onnx.checker.check_model(model) print(onnx.helper.printable_graph(model.graph))转换时最容易踩的坑是opset版本。太低的opset不支持某些动态shape算子比如Resize在opset 10之前表现很差我一般直接定到12兼容性和性能都够用。如果你发现某一次转换后在ONNX Runtime里报“不支持的算子”可以试试把opset升到13或更高但要先确认onnxruntime版本也跟得上。2.3 动态输入形状到底要不要固定这是部署PP-OCR系列最纠结的问题我展开细说。检测模型的输入是整张图片图片尺寸不固定所以检测模型导出时一般保留动态shape也就是输入的H和W都是-1。这样部署时才能喂任意尺寸的图后处理再根据缩放比例还原坐标。方向分类和识别模型的输入则建议固定高度、只保留宽度动态。识别模型的输入常见是高度32宽度动态方向分类模型则是固定高度48、宽度192基本不动态。固定高度之后ONNX Runtime能对卷积算子做更多优化推理确实快一点。如果你想让识别模型固定成[1,3,32,-1]这样的shape可以在转换命令里加参数。不同版本paddle2onnx的写法不太一样有的用--input_shape_dict有的直接传--input_shape。最简单的方式是先不指定任何shape导出后用ONNX Runtime打印一下输入看它实际要的是什么import onnxruntime as ort sess ort.InferenceSession(./onnx/ppocrv5_rec.onnx, providers[CPUExecutionProvider]) for inp in sess.get_inputs(): print(inp.name, inp.shape)打印结果里的shape如果显示[1,3,32,-1]那说明高度已经固定成32了直接跑就行。如果显示[-1,3,-1,-1]那就是完全动态你可以在服务代码里按高度32做resize后再喂进去效果一样。2.4 导出后的模型验证不能省转换完了一定要做一次输出比对不要直接接到服务上。方法很简单准备一张带文字的测试图分别用PaddleOCR原本的推理逻辑和ONNX Runtime推理逻辑跑一遍对比三个阶段的输出。我每次升级模型版本都会做这个验证。检测模型对比的是检测框坐标方向分类模型对比的是分类置信度识别模型对比的是最终字符串。哪一层对不上就单独排查那个模型的预处理或后处理。这种“逐层对齐”的排查方式能帮你快速定位问题省得盲目调参。3. DeploySharp推理管线设计3.1 整体架构与模块划分DeploySharp按功能划分成四个模块模型加载管理、预处理、推理执行、后处理。模型加载管理负责创建和复用ONNX Runtime会话预处理负责把图片转成模型需要的张量推理执行负责调用session.run后处理负责把模型输出转成最终检测框和字符串。这样拆分的好处是每个环节都可以单独测试。比如你怀疑识别精度有问题可以只对一张文本行图片跑预处理和识别模型输出中间结果不用每次都要跑完整管线。DeploySharp在初始化时会把三个模型的路径、执行提供方、线程数、字符表路径都集中到一个配置文件里切v4和v5只需要改配置不需要动代码。这一点在项目里价值很大因为你很可能今天用v4跑通明天就想着换v5试试效果。3.2 检测模型预处理缩放和归一化一个都不能错检测模型的预处理是整条管线里最容易写错的地方。PaddleOCR官方用于推理的DetResizeForTest逻辑是先按比例缩放图片使最长边不超过960像素然后把宽和高分别调整到32的整数倍最后做归一化。def det_preprocess(img, limit_side_len960): h, w img.shape[:2] ratio min(1.0, limit_side_len / max(h, w)) resize_h int(round(h * ratio / 32) * 32) resize_w int(round(w * ratio / 32) * 32) resize_h max(resize_h, 32) resize_w max(resize_w, 32) img cv2.resize(img, (resize_w, resize_h)) img img[:, :, ::-1].astype(float32) / 255.0 mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) img (img - mean) / std img img.transpose(2, 0, 1)[None].astype(np.float32) return img, ratio这里有三个细节必须注意。第一检测模型用的是ImageNet的mean和std也就是0.485、0.456、0.406那一组不是识别模型那组0.5、0.5、0.5。这两个预处理不一致是正常的但不能搞混。第二这里只做resize不要额外padding。很多人习惯在resize后补一圈黑边凑整这在检测上会导致后续坐标全部偏移。PaddleOCR官方逻辑是直接把图片resize到32的整数倍虽然会有轻微拉伸但后处理坐标只跟缩放比例有关反而简单。第三通道顺序问题。cv2.imread读进来是BGRPaddleOCR训练时用的预处理是在BGR图基础上转RGB的所以你在归一化之前要先把通道翻转一下也就是第6行的img[:, :, ::-1]。3.3 检测后处理DBNet概率图如何还原成文本框检测模型的输出是一张概率图形状一般是[1,1,H/4,W/4]里面每个像素点的值代表这个位置是文字中心的概率。后处理的核心步骤是二值化、找轮廓、计算最小外接矩形、按unclip比例外扩、坐标还原。def det_postprocess(prob_map, ratio, thresh0.3): prob prob_map[0, 0] binary (prob thresh).astype(np.uint8) * 255 contours, _ cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) boxes [] for cnt in contours: if cnt.shape[0] 4: continue area cv2.contourArea(cnt) if area 5: continue rect cv2.minAreaRect(cnt) points cv2.boxPoints(rect) # 这里可以按unclip系数外扩比例通常取1.5 # 实现可参考PaddleOCR的DBPostProcess用pyclipper对外围轮廓做二次拟合 points points / ratio x, y points[:, 0], points[:, 1] x np.clip(x, 0, int(prob.shape[1] * 4)) y np.clip(y, 0, int(prob.shape[0] * 4)) boxes.append(points.astype(np.float32)) return boxes很多人问unclip到底怎么做。简单理解DBNet输出的概率图是缩小的导致还原出来的框往往紧贴文字边缘直接送去识别会切掉一部分字符笔画所以要把框往外扩一点。PaddleOCR自己是用pyclipper对四边形做膨胀处理效果比简单缩放四个顶点更稳。如果你不想引入pyclipper可以直接对minAreaRect的宽高乘以1.5再按中心点重新生成四个顶点大多数场景也能用只是对倾斜文本的贴合度差一些。检测框坐标还原时还有一个容易被忽略的点输出概率图的宽高是输入图片的四分之一所以用prob.shape计算原图尺寸时要记得乘4不然clip边界会算错。3.4 方向分类别小看这个二分类模型方向分类模型的作用很明确判断裁剪出来的文本块是否需要旋转180度。虽然看起来只是二选一但在中文识别场景里倒置文本如果不纠正识别结果基本就是乱码。分类模型的预处理和识别模型一样用0.5、0.5、0.5这组mean/std输入尺寸固定为高48、宽192。推理输出是一个二维向量第一维代表0度置信度第二维代表180度置信度。def cls_postprocess(cls_output, thresh0.9): prob_180 cls_output[0, 1] need_rotate prob_180 thresh return need_rotate这里的阈值默认是0.9也就是说只有模型很有把握图像是倒置的才会去旋转。这样做是为了避免误旋转因为一旦把正着的图转180度识别结果必错。如果你在实际项目里发现漏检倒置情况比较多可以适当把这个阈值降到0.8但要注意误转率也会上升。3.5 识别模型把文本行图片变成字符串检测框和方向都处理完之后需要对每个检测框做透视变换裁出文本行图片然后送入识别模型。识别模型预处理的关键是固定高度宽度按比例缩放。比如模型要求高度32那么一张高为40、宽为200的文本行图片就要先按比例缩放到高32、宽160左右然后再归一化。注意这里用的是0.5、0.5、0.5这组mean/std不是检测那组。推理输出是一个[1, seq_len, num_classes]的矩阵需要做CTC解码也就是greedy decode。最粗的做法是每列取argmax然后把连续重复的字符合并、跳过blank最后用字符表映射成真正的字符串。字符表来自官方文件ppocr_keys_v1.txt里面按行记录所有字符索引和模型输出的类别一一对应。我这里给出一个简化版的解码函数def rec_postprocess(rec_output, char_list): rec_output rec_output[0] pred_index np.argmax(rec_output, axis1) text last None for idx in pred_index: if idx 0: last idx continue if idx last: continue text char_list[idx - 1] last idx return text这里idx0对应CTC blank实际不参与字符映射。字符表索引和模型输出类别之间错位1位是因为模型输出在开头多了一个blank位置。不同版本导出的模型可能在末尾还会多一个padding类这点在映射字符时尤其要注意一旦错位中文识别结果会出现大量乱码。3.6 完整管线串起来把三个模型串起来的时候流程就是读图转RGB跑检测预处理检测推理得到概率图检测后处理拿到候选文本框对每个文本框做透视变换得到文本行图片文本行图片传入方向分类如果分类结果是180度就把图片旋转180度预处理后送入识别模型CTC解码得到字符串按检测框顺序或自定义规则返回结果这个流程本身不复杂但要做到工程可用就不能每次请求都重新创建ONNX Runtime会话也不应该把每次推理的时间浪费在重复加载模型上。DeploySharp在初始化的时候就把三个session全部建好之后每次请求只做张量拷贝和推理。4. ONNX Runtime调参与性能优化4.1 会话配置和线程数怎么给ONNX Runtime的SessionOptions直接影响CPU推理性能。最关键的几个参数是线程数、图优化级别和内存复用策略。import onnxruntime as ort so ort.SessionOptions() so.intra_op_num_threads 4 so.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL so.enable_mem_pattern True sess ort.InferenceSession( ./onnx/ppocrv5_det.onnx, sess_optionsso, providers[CPUExecutionProvider] )intra_op_num_threads不要盲目设成服务器核数。ONNX Runtime默认线程数是核数但OCR服务往往是IO密集加推理密集混在一起线程数设太大反而导致上下文切换开销过高。我自己常用的经验是设成物理核心数的一半左右压测后再微调。比如8核机器先设4观察QPS和延迟的拐点。还有一个容易忽略的配置optimized_model_filepath。开启后ONNX Runtime首次加载模型时会把图优化结果缓存到文件下次启动直接加载优化后的模型能省一到两次第一次推理的预热时间。对服务端常驻进程帮助不大但对那种冷启动场景很友好。4.2 CPU、CUDA、TensorRT执行提供方怎么选ONNX Runtime的providers是一个列表按顺序匹配可用的后端。比如你写providers [TensorrtExecutionProvider, CUDAExecutionProvider, CPUExecutionProvider]那么有GPU且有TensorRT环境时就用TensorRT没有就退到CUDA再没有就退到CPU。这个设计很灵活但也容易埋坑很多机器上CUDA环境没配好ONNX Runtime悄悄fallback到CPU你以为是GPU在推理实际速度慢得要命。所以我建议在初始化时主动打印探测结果print(ort.get_available_providers()) print(sess.get_providers())get_providers()返回当前session真正使用的provider列表第一个就是实际执行后端一眼就能看出有没有走对。CUDA ExecutionProvider下如果想用FP16推理需要设置provider_optionsoptions [ {trt_fp16_enable: True, trt_engine_cache_enable: True, trt_engine_cache_path: ./trt_cache}, {}, {} ]TensorRT provider对动态shape支持有些限制。检测模型输入完全动态每次尺寸都可能变这种情况下TensorRT的重编译开销很大反而不如CUDA EP。所以DeploySharp默认的做法是检测模型用CUDA EP识别模型用TensorRT EP因为识别模型的输入宽度虽然动态但变化范围比较可控TensorRT的profile可以覆盖住。4.3 FP16量化和模型精简的实际收益FP16量化是GPU上最省事的加速手段。ONNX Runtime里CUDA EP和TensorRT EP都支持FP16关键是模型里有没有不支持的算子比如某些动态shape相关的Resize在FP16下可能报错。我实际测下来PP-OCR v5的检测和识别模型在TensorRT下开FP16推理速度大约能提升20%到40%精度损失在可接受范围内。但要注意方向分类模型尺寸很小开FP16的收益几乎可以忽略有时候反而因为算子转换产生额外开销所以DeploySharp对cls模型默认不开FP16。如果连CUDA都没有纯CPU环境想精简模型一个思路是转成动态量化过的ONNX模型但文本识别这类任务对精度比较敏感动态量化后中文识别率可能掉得比较厉害我不太推荐。CPU上第一优先级是调线程和图优化而不是量化。4.4 并发场景下会话复用的最佳实践ONNX Runtime的session不是严格线程安全的但同一个session可以在多个线程里串行执行推理也可以创建多个session分别处理多个并发请求。这两种方式各有适用场景。低并发场景比如QPS不到100一个复用session就够。高并发场景可以按CPU核心数创建多个session每个请求轮询分配一个session避免锁竞争。这里要注意的是session创建的代价不低初始化时一次性创建好不要每个请求new一个。GPU场景下多个session会占用更多显存。一般来说CUDA EP下复用单个session内部已经通过stream处理并发多session反而容易触发显存碎片。所以在GPU上我倾向于一个进程一个session用请求排队来解决并发。5. 常见问题与排查技巧实录5.1 推理结果和PaddleOCR对不上这是最高频的问题。现象往往是PaddleOCR直接跑出来的结果是对的自己的ONNX Runtime代码跑出来的框歪了、识别错了。第一步先检查预处理尤其是mean/std和通道顺序。检测、识别、分类三个模型的预处理不一样上面我已经写清楚了。第二步检查模型输出名的获取方式不要硬编码字符串用session.get_outputs()[0].name动态拿。第三步做逐层对齐把每层输出打印出来和PaddleOCR的中间结果对比定位是模型转换出了问题还是预处理出了问题。5.2 中文识别结果乱码乱码九成是字符表映射错位。ppocr_keys_v1.txt是按行存的字符每行一个字符加载时要注意大文件编码和末尾换行符。建议用readlines()后对每个元素strip()一次再跳过可能存在的空行。另外模型输出类别数量和字符表长度直接相关如果发现输出类别数比字符表长度多很多大概率模型转换时带上了额外的softmax头需要看下实际输出维度。还有一种情况就是把空白字符 当成了有效文本。CTC解码时连续重复字符要合并blank要跳过但空格字符是真实字符不能随便滤掉要在字典里找到空格对应的索引。5.3 转换报错或加载失败paddle2onnx转换报错时优先确认paddlepaddle和paddle2onnx的版本组合。有些报错信息看着像是模型有问题实际是paddle2onnx版本太老不支持新导出模型里的某个算子。升级paddle2onnx到最新版本往往能解决。加载ONNX时报No such file or directory但文件明明存在这种情况一般是路径里有中文或者session没有权限读文件跟模型本身无关。还有个常见原因是ONNX Runtime版本低于模型里某个opset要求升级runtime即可。5.4 内存持续上涨内存上涨最常见的原因是每次请求都重新创建session。如果确认session是复用状态再看是不是开了一堆没必要的模型副本。还有一种情况是enable_mem_pattern配合动态shape导致arena内存碎片越积越多。可以尝试把这个选项关掉或者使用固定shape输入实测能缓解一部分内存碎片问题。GPU显存上涨通常是因为CUDA EP的arena默认不释放显存。这不是泄漏而是ONNX Runtime为了后续推理预先保留了显存空间。如果你接受不了可以设置arena_extend_strategy为kSameAsRequested让显存按需申请减少峰值占用但运行速度会略降。5.5 问题速查表现象可能原因处理建议检测框整体偏移后处理没按缩放比例还原坐标检查ratio是否作用到位检测框大量缺失二值化阈值过高或unclip系数太小调低thresh适当增大unclip识别全是乱码字符表映射错位打印模型输出维度对照字符表长度识别输出为空白模型输入宽度过小resize比例异常识别预处理时限制最小宽度怎么调都是CPU在跑providers配置顺序问题打印session.get_providers()确认首次推理特别慢图优化和内存模式初始化用optimized_model_filepath缓存6. 从DeploySharp到项目落地的几条经验6.1 模型选择要不要追新如果你的系统已经在用PP-OCR v4不要急着换v5。v5精度更高速度也快但部署代码里的一些细节可能不一样比如识别模型的高度、某些预处理参数。DeploySharp的配置化设计就是为了让你能低风险切换。我建议先备份v4的ONNX文件再切v5用同一批验证图片跑A/B对比确认关键场景没有退化再全量切。6.2 动态batch值得做吗ONNX Runtime支持把多个文本行图片合成一个batch送入识别模型这样能充分利用GPU并行能力。但要注意OCR场景里每个文本行的宽度不一致合成batch时需要padding到相同宽度可能浪费不少计算。我实测发现batch为2到4时收益最大超过8反而因为padding浪费太多导致收益下降。实现方式并不复杂先把多个文本行图片各自resize到固定高度和各自宽度再按最大宽度pad成相同尺寸拼成[N,3,H,W]的张量一次推理。DeploySharp里默认不开启动态batch留了开关需要高吞吐时再打开。6.3 最后再分享一个调参细节所有模型参数里最值得花时间调的是检测后处理的unclip系数和二值化阈值。不同业务场景里的文字密度和清晰度不一样文档扫描件和手机随手拍的照片最优参数完全不同。我习惯是先在验证集上跑一组对比遍历thresh取0.2到0.5、unclip取1.2到2.0找到哪个组合对当前数据集最友好再写进配置。这个过程看起来很笨但比事后反复调整要高效得多。部署这件事模型是固定的让结果产生差异的往往就是这些预处理和后处理的细节。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑