ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

PP-OCR部署实战:从OpenCV DNN到TensorRT与自研推理引擎

PP-OCR部署实战:从OpenCV DNN到TensorRT与自研推理引擎 PP-OCR这套模型在中文OCR领域是真的能打PaddleOCR生态把检测、方向分类、识别整合成一条完整的推理链路开箱即用效果就很稳。但真正到了生产落地阶段事情就没那么简单了Python环境一换就崩、GPU机器贵还挑卡、边缘设备装不上依赖、Java后端想调OCR还得起一个独立服务。这些问题我前前后后折腾了将近两年干脆做了5个和PP-OCR相关的开源项目从OpenCV DNN到TensorRT再到纯C和纯Java自研推理引擎把不同场景下的部署路线都趟了一遍。这篇就把这5条路线的选型逻辑、核心实现、性能数据和踩坑现场一次说清楚。如果你是做部署工程化的、在写嵌入式视觉方案的、或者被Java服务集成OCR折磨过的这篇应该能帮你少走不少弯路。1. 项目全景5个仓库分别解决什么问题1.1 从零开始构建的5个PP-OCR项目先交代一下背景我做的这5个项目不是凭空冒出来的而是跟着实际需求一步步长出来的。最初只是想在本地快速验证PP-OCR能不能跑通后来发现验证归验证上线归上线边缘部署又是另一套玩法最后才演化出这5个仓库。项目一是个OpenCV DNN版Demo直接用OpenCV的DNN模块加载PP-OCR导出的ONNX模型不依赖PaddlePaddle框架本身。这个版本的好处是OpenCV几乎无处不在Windows、Linux、macOS甚至树莓派都能装写完就能跑适合快速验证模型效果和做原型演示。项目二是TensorRT加速版目标是解决GPU服务器上的高并发OCR请求。用的是TensorRT对ONNX模型做图优化和FP16半精度推理把检测、方向分类、识别三个模型串成一条GPU管道压测下来吞吐量比纯Paddle Inference快了不少。项目三开始“自研”了是一个纯C语言实现的PP-OCR推理引擎不依赖任何第三方库连OpenCV都不要。整个引擎只用了标准C和少量POSIX线程接口模型权重通过一个二进制解析器加载卷积、池化、全连接、Softmax这些算子全部手写。项目四是纯Java实现的自研推理引擎动机特别现实公司内部Java服务想接OCR但不想为了调一个功能额外维护Python服务也不想折腾JNI。干脆把引擎用Java重写了一遍直接打成Jar包丢进后端服务里啥额外依赖都不用装。项目五是前面四个的统一压测与封装仓库专门写了一套基准测试脚本和对比报告把OpenCV、TensorRT、纯C、纯Java在相同输入下的耗时、内存占用、模型大小、精度差异全部量化对比出来。这个仓库对后面做方案选型帮助最大没有这份量化数据很多决策都只能靠拍脑袋。1.2 从“调库”到“自研”的决策逻辑很多人问我为什么非要折腾纯C和纯Java直接用Paddle Inference不香吗说实话Paddle Inference在性能上确实没得挑它是Paddle官方团队针对自家模型做深度优化的推理引擎ARM、x86、GPU、NPU全平台覆盖。但问题恰恰出在“全平台覆盖”这几个字上。Paddle Inference的部署包体积动辄几百MB依赖的底层库如MKL、OpenBLAS、CUDA、cuDNN又特别挑剔版本。在服务器上还能接受可一旦到了嵌入式设备、国产化环境、或者Java后端这种需要极度精简的集成场景这套方案就很难受了。更麻烦的是Paddle的Python版本和C版本之间偶尔会有行为差异模型效果明明在Python里验证过了换成C推理却对不上排查起来极其痛苦。所以我的决策逻辑是这样的能用OpenCV DNN解决的就不上完整框架因为OpenCV的DNN模块是纯C实现的跨平台能力极强部署时只需要关注OpenCV一个库需要极致GPU性能的才上TensorRT毕竟TensorRT对NVIDIA GPU的优化力度是其他框架比不了的剩下那些对体积、依赖、环境有极端要求的场景才轮到自研引擎出场。自研引擎的核心收益是“零依赖”和“完全可控”。零依赖意味着拷一个可执行文件过去就能跑不挑操作系统、不需要装库、不怕版本冲突完全可控意味着每一个算子的实现、每一块内存的分配都在自己手里出问题可以定位到具体某一行代码而不是在黑盒里猜。代价也很明显就是开发和维护成本高这也是为什么我把项目三和项目四做成开源希望能让后来者少踩一些我已经踩过的坑。2. 不同方案的核心技术拆解2.1 OpenCV DNN版本快速跑通的关键与算子兼容性OpenCV DNN模块本质上是一个轻量级推理框架支持读取ONNX、TensorFlow、Caffe等格式的模型。它的优点是简单、跨平台、无重型依赖缺点是算子支持度比专用推理框架差一截对模型中的某些自定义算子无能为力。PP-OCR这个模型在导出ONNX后绝大部分算子OpenCV DNN都能处理但有几个点必须提前留意。第一是PaddlePaddle导出模型时会在模型里附加一些Paddle特有的算子比如paddle.nn.functional.grid_sample在PP-OCR文字检测模型里出现过多次老版本OpenCV不支持这个算子只能通过升级OpenCV或者在导出时用paddle2onnx把自定义算子替换掉来解决。第二是整个OCR推理链路里的预处理和后处理逻辑OpenCV的DNN模块只负责网络的forward运算图像缩放、归一化、通道调整、文本框解码、CTC解码都得自己写。我用OpenCV DNN跑通PP-OCR时的实际流程是这样的先安装带DNN模块的OpenCVLinux下可以用pip install opencv-python但生产环境建议自己cmake编译后面会有说明然后用paddle2onnx把Paddle模型转成ONNX这里注意导出时要把动态shape打开否则后面换不同分辨率图片会直接报错最后用OpenCV的readNetFromONNX加载模型调用forward得到输出特征图。在预处理阶段有一个很隐蔽的坑PaddleOCR官方代码里图像归一化用的是(img / 255 - mean) / std其中mean和std对RGB三个通道分别取值而且注意图像读进来是BGR还是RGB一旦搞反识别准确率会肉眼可见地下降。我自己在这个问题上吃过亏——当时用OpenCV读图默认是BGR而PP-OCR训练时用的是RGB必须做一次通道翻转结果忘了后面的所有实验精度都低了2到3个百分点。2.2 TensorRT版本性能优化背后的格式与显存管理TensorRT是NVIDIA推出的高性能推理优化器它会对计算图做层融合、精度校准、内核自动调优在GPU上能把模型速度压榨到很极限的地步。但TensorRT只支持NVIDIA的GPU而且不同版本的TensorRT对GPU架构有不同的兼容要求。网上有网友问“TensorRT 10.x是否支持GTX 1070”这个问题其实是很多老卡用户都会踩的坑。GTX 1070是Pascal架构计算能力6.1TensorRT 8.x及之前的版本对Pascal架构支持还算友好但从TensorRT 9.x开始NVIDIA明显把优化重心移到了Ampere和更新的架构上虽然Pascal还能跑但部分新特性如稀疏化、某些INT8校准方式已经不再支持。到了TensorRT 10.x实测在GTX 1070上跑PP-OCR模型FP16推理是可以正常工作的但性能提升幅度比30系、40系卡小很多。如果你手头只有老卡建议还是用TensorRT 8.x LTS版本更稳妥。TensorRT的使用流程一般是三阶段ONNX转engine、engine序列化保存、反序列化推理。转engine这一步可以用trtexec命令行工具也可以直接用TensorRT的Python/C API。这里最需要注意的是动态shape和显存分配。PP-OCR的检测模型输入尺寸通常不是固定的因为图片长宽比差异大固定尺寸会浪费大量计算。我采用的是动态shape策略把输入的最小和最大范围都传给TensorRT让它为每个可能的分辨率生成对应的优化内核。这样做有一个副作用第一次推理时TensorRT需要为某个shape创建执行上下文耗时可能高达几十秒所以生产环境一定要做engine预热在服务启动时先跑一遍典型尺寸的输入把所有可能的优化内核都加载好。另外还有一个我反复强调的坑TensorRT的engine文件跟GPU型号和TensorRT版本强绑定换机器必须重新生成。如果你的生产环境有不同型号的GPU千万不要图省事拷同一个engine文件过去否则直接崩溃或者输出全为NaN。之前我在项目里加了自动检查逻辑把GPU型号和TensorRT版本号作为哈希因子拼进engine文件名这样就不会拿错文件了。2.3 纯C自研引擎把依赖砍到零之后需要面对的底层细节做纯C引擎的出发点其实挺朴素树莓派上装Python环境太折腾装OpenCV又慢又占空间要是能有一个不带任何依赖的原生可执行文件直接拷贝过去就能跑那就太省事了。于是项目三开始了。纯C引擎的核心工作可以拆成三块模型文件解析、张量运算实现、推理上下文管理。模型文件解析指的是读取ONNX格式的模型并把网络结构重建出来。ONNX本身是一种Protobuf格式正经做法是引入protobuf库来解析但那样又违背了“零依赖”的初衷。我采用的是折中方案用官方onnxPython库把模型预处理成一个自定义的二进制格式只保留必要的算子类型、权重数据和张量shape信息C引擎只需要按约定好的字节顺序去读这个精简格式就行。这套方案能把模型文件压缩到原ONNX体积的60%左右而且解析代码只有几百行。张量运算实现是最大的工程PP-OCR检测和识别模型包含的算子不算复杂但手写起来依然是个体力活。以卷积为例标准实现是NCHW布局下的滑窗计算我做了两层优化第一层用im2col把卷积转成矩阵乘法方便调用优化过的GEMM函数第二层对权重做内存重排把通道维对齐到SIMD宽度比如AVX2的32字节提高缓存命中率。纯C引擎里我没有用OpenMP而是用pthread开了一个线程池在每个卷积层上做数据并行。精度对齐是这个阶段最折磨人的环节。手写算子最大的风险是浮点运算顺序和Paddle/TensorRT不一致导致输出特征图的数值在小数点后几位出现细微偏差累积到最后预测结果完全对不上。我的调试方法是逐层对比用Python的Paddle Inference跑一遍把每一层中间输出dump成npy文件再用C引擎加载同样的输入逐层打印中间结果两者做余弦相似度对比。哪一层误差大了就检查哪一层通常问题都出在卷积实现里累加顺序不一致或者是BatchNorm的epsilon参数取值差异。2.4 纯Java自研引擎跨平台服务化集成的最终解法做完纯C引擎之后按说已经能满足大部分部署场景了但真正让我下决心再写一个Java版引擎的是公司内部一个具体需求一个用Spring Boot写的老服务需要临时加一个身份证照片信息提取功能OCR识别是核心环节。当时有两个选择一是单独部署一个Python OCR服务通过HTTP接口调用二是用JNI调C引擎。两个方案都让运维和研发很头疼前者多了一个要守护的进程和网络调用延迟后者在Windows和Linux上都要重新编译native库环境一变就崩。于是就有了项目四纯Java实现的PP-OCR推理引擎。Java版不能依赖OpenCV的Java包装因为OpenCV官方Java包在不同系统上需要不同的native库又回到了老问题上。所以整个引擎从图像解码、缩放、归一化到卷积、池化、Softmax全部用Java原生实现。Java引擎的设计里最核心的是一个FloatTensor类它负责管理N维浮点数组的shape、strides和底层存储所有算子都围绕这个类展开。卷积实现没有照搬C版的im2col因为Java里数组访问不如C指针灵活我改用了直接滑窗但做了边界判断和循环展开实测效率虽然比C版慢一些但在现代CPU上跑一个识别模型也就几十毫秒对业务场景完全够用。Java版另一个要考虑的问题是GC对推理延迟的影响。模型推理时需要频繁创建中间张量如果每层都new一个新数组GC压力会很大。我的解法是引入一个简单的内存池把shape相同的中间张量缓存起来复用每轮推理结束后归还。这个优化让Java版的P99延迟降低了大概40%效果非常明显。到这里5个项目算是齐了。下面我把其中最有代表性的实操过程展开讲从模型导出到实际推理的完整链路尤其是一些容易翻车的细节。3. 实操过程与关键环节实现3.1 模型准备从PaddleOCR导出ONNX不论你用OpenCV、TensorRT还是自研引擎第一步都是把PaddleOCR训练好的模型转出来。PaddleOCR最新版本以PP-OCRv4为例包含三个子模型文本检测模型、方向分类模型、文本识别模型。每个模型都有一个inference模型文件和一个yml配置文件推理时通过配置文件里的参数决定预处理和后处理方式。模型导出最简单的方式是用PaddleOCR官方提供的tools/export_model.py脚本它会把训练好的权重和网络结构固化成两个文件inference.pdmodel和inference.pdiparams。然后需要再用paddle2onnx命令行工具把这套Paddle文件转成ONNX命令大概是paddle2onnx --model_dir ./inference/ch_PP-OCRv4_det_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ./output/ch_PP-OCRv4_det.onnx \ --opset_version 11 \ --enable_onnx_checker True这里--opset_version建议用11太低的版本某些算子在OpenCV里表达不完整太高的话TensorRT老版本又不认识。--enable_onnx_checker是ONNX官方语法检查强烈建议留着它能第一时间发现导出文件结构是否有问题。导出ONNX时最常见的坑是动态shape配置。PaddleOCR默认导出的模型可能是固定shape比如[1, 3, 640, 640]一旦输入图片分辨率不是640x640就会报错或者被强制resize。解决方法是导出时设置--dynamic_shape True让模型的输入维度成为动态的。但注意动态shape会显著增加TensorRT转换时的显存和转换时间如果服务端图片分辨率相对固定建议导出多个固定shape的engine而不是无脑开动态。3.2 TensorRT完整推理链路从trtexec到自定义Pipeline有了ONNX模型下一步就是用TensorRT生成优化后的engine。命令行的trtexec工具可以快速验证模型能否转换成功限制条件是它只能做前向推理不支持自定义的后处理。我通常用它来做模型兼容性检查和性能基准测试完整业务逻辑还是得写C或Python代码。trtexec --onnxch_PP-OCRv4_det.onnx \ --saveEnginech_PP-OCRv4_det_fp16.engine \ --fp16 \ --minShapesinput:1x3x480x480 \ --optShapesinput:1x3x640x640 \ --maxShapesinput:1x3x960x960 \ --workspace4096这里--minShapes、--optShapes、--maxShapes分别指定动态输入的最小、最优、最大尺寸。--workspace是TensorRT允许使用的显存上限设置太低会导致优化时内核选择受限太高也不会分配全部给模型运行只是个上限约束。TensorRT推理时的完整pipeline包括图像预处理解码、缩放、归一化、模型推理、后处理文本框检测解码、NMS、CTC解码几个模型的衔接也值得展开。文本检测模型输出的是概率图和阈值图需要经过DB解码才能得到文本框。思路是对概率图做固定阈值二值化比如0.3然后找连通域用连通域的最小外接矩形来表示文本框。这个步骤如果用OpenCV做就是threshold加findContours加minAreaRect但TensorRT的C环境不一定有OpenCV所以我在项目三里也用纯C实现了二值化和连通域标记效果虽然不如OpenCV的算法完善但胜在无依赖。文本识别模型输出的是每个时间步在字符类别上的概率分布要得到最终文本需要用CTC解码。PP-OCR的字符集文件ppocr_keys_v1.txt包含中文字符、英文字符和特殊符号解码时把概率最大的类别索引映射回字符然后去除连续重复字符和blank符。这里有个细节PP-OCR的识别模型除了输出字符序列还有ctc_greedy_decoder和ctc_beam_search_decoder两种解码方式。贪心解码实现简单速度飞快但在中文字符存在形近字、生僻字时容易出错beam search效果好一些但实现复杂度高在C/Java引擎里我暂时只实现了贪心解码服务端需要更高准确率时可以回退到Paddle/TensorRT的方案。3.3 纯C推理的矩阵乘法优化与内存布局如果只是把模型跑通那纯C引擎的代码结构其实不复杂。但要做成能实际部署的引擎性能优化是绕不开的。PP-OCR模型的骨干网络是类似ResNet的结构每一层卷积都要做大量的乘加运算矩阵乘法的速度直接决定整体推理耗时。我用到的第一个优化技巧是im2colGEMM。先把输入特征图按照卷积核大小展开成一个大矩阵然后调用针对当前CPU微架构优化过的矩阵乘法函数。GEMM本身我没有手写能用BLAS库就用BLAS库只在不能用BLAS的地方才退回到自己写的多线程矩阵乘法实现。实测在树莓派4B上im2colGEMM相比直接滑窗卷积提速大约3到4倍。第二个优化是内存布局。ONNX里张量默认是NCHW布局通道维在HW之前卷积计算对NCHW并不友好因为每次取一个卷积窗口都要跳很远的内存地址。我把卷积层的输入和输出都重排成NHWC布局通道维在最后这样窗口内的像素在内存上是连续的SIMD加载和缓存命中率都大幅提升。代价是每层之间多了一步布局转换但卷积层计算量远大于转换开销整体还是净赚。第三个优化是BatchNorm折叠。PP-OCR识别模型里每个卷积后面基本都跟着一个BatchNorm层推理时BatchNorm的均值、方差、缩放因子和偏置都是固定的可以提前融合到卷积层的权重和偏置里。这样推理时省去了整整一层的内存读写和计算而且精度完全不受影响。这个优化属于“白拿”的我在OpenCV和TensorRT方案里也验证过效果一致。3.4 Java引擎的无第三方依赖实现细节Java引擎的设计目标是不依赖任何第三方库这意味着图像解码也得自己写。JPEG解码器的实现代码量太大我的取巧做法是只支持不带压缩的BMP图像作为引擎输入然后在上层接口里用一个极简的PNG解码器转换这样引擎本身聚焦在张量计算上图像解码交给调用方处理。但项目末尾我写了一个适配层把BufferedImage转成RGB浮点数组这样Java服务端直接用ImageIO.read读图也算无缝衔接。推理部分的Java实现里卷积层同样走直接滑窗路线但做了两个Java特有的优化一是用ThreadPoolExecutor固定线程数做并行线程数设为CPU核心数减一避免和主业务线程争抢核心二是对每一层输出的临时张量复用对象避免频繁触发Young GC。Java引擎跑一个PP-OCR识别模型的性能数据是在8核Intel Xeon处理器上识别一张320x32的文本行图片单线程约35ms4线程并行约12ms。作为对比Python Paddle Inference在同样的CPU上大约是20msOpenCV DNN约25ms。Java版慢一些但对多数业务接口来说12到35毫秒完全可以接受而且它省掉了进程间通信和HTTP序列化的开销整体服务链路延迟反而更低。下面整理一张性能对比表格是我在第5个仓库里压测得到的典型数据具体数值会因CPU型号和图片分辨率波动但量级关系是稳定的。方案推理后端单次识别耗时320x32文本行依赖体积跨平台难度Paddle InferenceCPUPaddle自研约20ms200MB中OpenCV DNNOpenCV约25ms80MB左右低TensorRTFP16NVIDIA GPU约3ms600MB含CUDA高仅N卡纯C引擎手写算子了约15ms不到200KB极低纯Java引擎手写算子约12ms4线程300KB Jar包极低这张表最能说明自研引擎的价值所在体积小了一个数量级跨平台方便程度也不是传统方案能比的。4. 常见问题与排查技巧实录4.1 模型加载失败与算子系统兼容性速查表不管用哪种方案模型加载失败都是第一道坎。我在不同方案里遇到的典型报错和解决办法整理如下。报错场景常见原因解决方案OpenCVreadNetFromONNX报错模型包含DNN模块不支持的算子升级OpenCV到4.7用paddle2onnx时设置--custom_ops或替换不兼容算子TensorRT转engine失败动态shape配置错误或显存不足检查minShape/optShape/maxShape范围减小--workspace换TensorRT 8.xengine加载后输出全为NaNGPU型号与生成engine的机器不一致重新生成engine文件名中带GPU型号标识自研C引擎解析模型崩溃二进制格式字节序与解析器约定不一致统一用小端字节序解析时打印关键结构体字段做对照Java引擎卷积输出维度和Python不一致shape计算时padding或stride参数错误逐层对比Python中间输出定位第一层不匹配的算子单独说一下OpenCV版本的问题。网上经常有人问“如何安装OpenCV”或者“在conda里装不上opencv怎么办”。我的建议是开发调试可以用pip install opencv-python快速安装但生产环节如果对性能和稳定性有要求还是要自己从源码编译。编译命令大致是git clone https://github.com/opencv/opencv.git cd opencv mkdir build cd build cmake -D CMAKE_BUILD_TYPERELEASE \ -D CMAKE_INSTALL_PREFIX/usr/local \ -D WITH_CUDAON \ -D WITH_CUDNNON \ -D OPENCV_DNN_CUDAON \ -D WITH_TBBON .. make -j$(nproc) sudo make install这里WITH_CUDA和OPENCV_DNN_CUDA如果打开OpenCV DNN就能利用GPU加速。但要注意OpenCV自带的DNN-CUDA加速能力远不如TensorRT如果真是GPU场景直接用TensorRT会更划算。4.2 精度对不上从“识别结果错”倒查算子实现自研引擎跑出来的识别结果跟Python推理不一致这是最多人问的问题。造成精度不一致的原因通常可以归结为以下三类。第一类是浮点运算顺序不同。C语言的浮点运算顺序和Python的numpy调度方式不同累加顺序变化会导致结果在小数点后几位有差异经过多层网络累积后可能在Softmax输出上体现为置信度差几个百分点。这类问题不影响最终字符解码忽略即可如果非要和Python输出完全一致需要在计算卷积时指定累加顺序例如全部转成double再相加但会牺牲性能。第二类是预处理参数不一致。这是最容易被忽略的也是导致精度大幅下降的头号原因。PP-OCR识别模型的输入归一化参数是mean[0.5, 0.5, 0.5]、std[0.5, 0.5, 0.5]而OpenCV DNN的blobFromImage默认不会自动做这个归一化需要自己指定mean和scale。比如设置scale1/0.5/255、mean0.5/0.5这样换算才能和训练时的预处理完全对齐。第三类是后处理细节差异。检测模型的DB解码里有一个box_thresh参数识别模型的CTC解码里有blank索引和去重规则。这些参数在不同语言实现里取值不一致会导致同一个模型的输出文本不同。我的做法是写一个针对后处理逻辑的单元测试固定输入特征图验证Python和自研引擎解码出来的文本框、文本内容完全一致。4.3 内存与显存优化的几条亲测有效的经验推理引擎的内存占用是另一个常见瓶颈尤其是在Java服务里一个引擎对象如果管理不好内存泄漏和GC抖动能把整个服务拖垮。TensorRT方案里最关键的是显存复用。PP-OCR的三个模型如果每个都独立创建执行上下文显存占用会翻三倍。正确做法是让三个模型共享同一个CUDA stream并且把输入输出buffer都复用同一块显存。显存不够时TensorRT内部还会自动做一些显存池优化前提是不要每次都创建新的ExecutionContext而是用enqueueV2重复调用同一个context。纯C引擎的内存管理要靠自己设计内存池。我把推理过程中的所有中间张量都分配在一大块预申请的内存里每个算子执行前从池里取一块执行完立即归还。这样整体内存占用几乎恒定不会出现频繁malloc/free导致的碎片化。实测一个PP-OCR识别模型在纯C引擎里峰值内存可以控制在8MB以内这对嵌入式设备非常友好。Java引擎的内存管理除了前面提到的张量复用还有一个隐藏很深的坑模型权重如果直接以float数组形式放在类里Java的类加载机制会把整个数组对象放进老年代一旦几千个类的权重数组初始化完成JVM堆占用可能瞬间飙升几百MB。我的解决办法是不把权重放在类文件里而是放到资源文件中按需加载配合ByteBuffer.allocateDirect做堆外内存存储能显著降低堆内压力。4.4 动态分辨率与长文本识别的特殊处理实际OCR场景里不可能所有图片都是正方形PP-OCR检测模型对长宽比大的图片特别容易漏检。我刚跑TensorRT版本时拿一个1920x1080的全景图直接送进去检测框漏了差不多三分之一后来才发现问题出在输入分辨率预设上。PP-OCR的检测模型有一个limit_side_len参数默认是960。意思是最长边会被缩放到960短边按比例缩放。在TensorRT动态shape下如果只设置了maxShape为1x3x960x960实际输入1920x1080的图缩放后是960x540这个尺寸没超过上限按理能跑。但如果我设置的是固定shape为1x3x960x960那就会出现问题输入被直接拉伸成正方形文字变形严重检测框自然就歪了。解决办法是给TensorRT引擎配置多档动态shape比如把optShapes设为1x3x576x960这样适配横图的比例并且对超大分辨率的图片先切分再检测检测结果合并后做去重。这个“切分合并”策略在长图识别场景里几乎必不可少单独的引擎能力再强也扛不住一张宽度上万的电商长图。长文本识别也类似PP-OCR识别模型的训练数据大多是不超过25个字符的文本行如果输入一行50个字识别模型的输出序列会截断。工程上的处理方法是调用方根据检测框的宽度和字符数预估结果如果单行文本太长就按一定比例切分成多段分别识别后再拼接。这个逻辑虽然简单但能显著提升长文本场景的准确率。5. 个人经验总结与后续扩展思路5.1 什么时候应该自研推理引擎很多同行问我你费这么大劲写纯C和纯Java的引擎到底值不值我的回答是分场景。如果你的OCR服务部署在标准的x86服务器上能接受Python环境那直接用Paddle Inference就够了它性能好、生态全、官方维护、坑都被人踩平了。如果你的OCR服务跑在GPU上而且显卡是NVIDIA的TensorRT是绕不开的最优解没有之一。OpenCV DNN适合快速原型和中低性能要求的应用胜在“一份代码到处跑”。自研引擎真正有价值的场景是这几个一是嵌入式设备和单片机系统内存和存储空间有限容不下几百MB的推理框架二是跨平台桌面应用比如需要在Windows、macOS、Linux上分发同一个安装包又不希望用户去配Java或Python环境三是Java服务端集成不想引入JNI和native库也不想额外部署一条Python服务。最后一个场景在实际企业开发里特别普遍所以我觉得Java版引擎的投入产出比其实是最高的。5.2 后续可以继续扩展的方向这5个项目目前只是把PP-OCR的基础推理链路打通了后续能扩展的方向还挺多。第一是量化。目前纯C和Java引擎都只支持FP32推理如果能支持INT8量化推理速度还能再上一个台阶。不过量化会带来精度损失需要做校准集和精度回测这块工程量大适合作为下一步的优化重点。第二是动态shape的进一步适配。目前自研引擎在模型加载时就把输入shape固定了遇到长宽比特别极端的图片得靠上层切图逻辑来兜底。后续可以考虑在引擎内部加一个按比例缩放的预处理层让模型输入保持等比不变形。第三是产品化封装。把纯C引擎封装成HTTP服务或在Java引擎上做Spring Boot Starter让业务方接入时只需要加入一个依赖、配置一行代码就能用上OCR能力。做到这一步几个项目的完整价值才真正显现出来。5.3 给同样在做OCR工程化的朋友一点建议做这种底层自研项目最大的敌人不是技术难度而是“做到一半想放弃”的冲动。我写纯C引擎的卷积优化时连续两周都在跟访问越界和segment fault斗争调试器里看到的内存地址跳跃比看股票还刺激。但熬过那段之后整个推理引擎的运行逻辑在脑子里就变得非常清晰后面加新算子、调性能都快得多。另外强烈建议一定写一个独立的对比测试脚本把Python版本、TensorRT版本和你自研引擎的中间张量逐个对比。没有这个脚本你会陷入“为什么输出不对”的泥潭里无法自拔。我现在每改动一个算子第一件事就是跑一遍“中间层对比测试”只要有一次余弦相似度低于0.999就马上知道是哪层出了问题。这套方法也是我在整个项目中收获最大的工程经验比单纯把引擎跑通更值钱。
返回列表