尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PaddleOCR Windows C++部署实战:CMake+VS2017 CPU推理完整指南

PaddleOCR Windows C++部署实战:CMake+VS2017 CPU推理完整指南 做Windows下的OCR部署绕不开PaddleOCR的C推理。这个活儿说难不难但坑是真不少。尤其是“导出库的版本”这个事版本选不对后面编译报错、运行闪退、结果乱码一环扣一环。我自己在CMake VS2017这套组合下踩了整整一轮坑才把CPU版本的推理流程跑通这篇文章就把整个部署思路、版本匹配逻辑、关键代码和避坑经验完整捋一遍。这个方案解决什么问题就是你训练好或者下载好的PaddleOCR模型要在纯Windows环境、没有GPU、只用CPU的情况下用C接进自己的业务系统。适合所有需要在Windows客户端做本地OCR识别、又不想依赖Python运行时或者HTTP服务的开发者参考。整套流程跑通之后你会对Paddle Inference在C侧的完整调用方式、预处理细节和后处理逻辑有非常清楚的认识。1. 开局先定版导出库版本怎么选才对1.1 推理库版本号里藏的信息标题里“导出库的版本”这几个字看着不起眼实际上是最容易出问题的地方。PaddleOCR的C推理依赖Paddle Inference预测库这玩意儿的版本号是有讲究的。以paddle_inference-v2.3.2-win-x64-lib.zip为例拆开看就是paddle版本是2.3.2平台是Windows架构是x64lib后缀表示这是纯库文件包。下载页面上还会看到avx和noavx两个分支。如果你的CPU是近10年内的酷睿、锐龙基本都支持AVX指令集选cpu_avx_mkl版本数学库用的MKL矩阵运算快不少。如果是老机器或者嵌入式类CPU不确定的话就选noavx兼容性优先。还有GPU和CPU的区分。哪怕你机器上有NVIDIA显卡但如果只是为了跑CPU推理也建议直接下CPU专用版本体积小、依赖少。GPU版本的库会带着CUDA和cuDNN相关依赖会让整个部署环境复杂很多纯CPU场景完全没必要。这里有个比较实际的教训我一开始图省事下了GPU库想在CPU上跑结果初始化报错找不到cudart64_110.dll还得多装一堆运行库纯属自己给自己加戏。1.2 模型版本与推理库版本的匹配关系选推理库的时候还要考虑和模型文件的匹配。PaddleOCR官方的推理模型下载页会标注模型对应的Paddle版本。比如2.x版本的推理模型用2.3.x的推理库基本没什么问题但如果你拿一个3.0版本导出的模型去用2.3版本的推理库大概率会出现算子不支持或者IR解析失败的情况。逻辑很简单预测库相当于解释器模型相当于代码解释器太老跑不了新代码。一个建议下载推理模型时优先选和推理库同代版本不要跨太多版本。比如推理库用2.3.2推理模型就选2.3或2.4系列。PaddleOCR的文本检测、方向分类、文本识别三个模型都要分别下载推理版本这是三个独立的推理模型文件各自的inference.pdmodel和inference.pdiparams都要准备好。值得一提的是新版模型大多数是动态shape两个文件就行老版本可能多一个inference.pdiparams.info影响不大。1.3 VS2017和CMake的安装设置VS2017安装时一定要勾选“使用C的桌面开发”这个工作负载会把MSVC v141编译器、Windows SDK、CMake工具一起带上。顺手把“VC 2017版本15.9 v14.16最新v141工具集”也勾上避免某些组件缺失。CMake我建议不要用VS自带的那个版本直接去CMake官网下载3.20以上的Windows x64版本安装时选择加入系统PATH后面命令行操作会方便很多。这里有些容易出错的地方值得提前注意。第一VS2017、VS2019、VS2022可以共存但Paddle Inference的编译产物是按编译器版本区分的下载页一般会说明是msvc2017还是msvc2019编译的。我们既然用VS2017就要选对应msvc2017版本的库文件否则链接阶段会有大量符号不匹配报错。第二x86和x64的问题Paddle官方库都是x64版本VS2017里整个工具链也必须切到x64。项目配置的时候选Win32就是末日一堆链接错误等着你。2. 工程结构设计与CMake配置落地2.1 目录怎么摆依赖怎么放工程结构的规划直接决定后面CMake配置的复杂度。我的建议是搞一个干净的目录把第三方依赖单独放一个third_party文件夹避免和源码混在一起。具体结构可以参考这样ocr_deploy/ ├── CMakeLists.txt ├── src/ │ └── main.cpp ├── models/ │ ├── det/ │ │ ├── inference.pdmodel │ │ └── inference.pdiparams │ ├── cls/ │ │ └── ... │ └── rec/ │ └── ... └── third_party/ ├── paddle_inference/ └── opencv/Paddle推理库解压之后大概会有一个paddle_inference根目录里面包含paddle、third_party安装时生成的依赖、bin、lib、include几个子目录。OpenCV我用的是4.5.5的Windows x64版本。版本不用太纠结4.x的任意稳定版都行但要保证编译库用的VS版本也是2017或兼容版本不然链接时也会出问题。2.2 CMakeLists.txt核心配置逐行解析CMake配置是整个编译过程的核心。我要特别强调一下CMAKE_BUILD_TYPE在VS2017这种多配置生成器下不用设置而是通过--config Release来指定这个区别很重要。完整CMakeLists可以这样写cmake_minimum_required(VERSION 3.15) project(ocr_deploy) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 指定Paddle推理库路径 set(PADDLE_INFERENCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/paddle_inference) set(PADDLE_LIB_DIR ${PADDLE_INFERENCE_DIR}/paddle/lib) set(PADDLE_INCLUDE_DIR ${PADDLE_INFERENCE_DIR}/paddle/include) set(PADDLE_THIRD_PARTY_DIR ${PADDLE_INFERENCE_DIR}/third_party) # 指定OpenCV路径 set(OpenCV_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/opencv) find_package(OpenCV REQUIRED) include_directories( ${PADDLE_INCLUDE_DIR} ${PADDLE_THIRD_PARTY_DIR}/install/mkldnn/include ${PADDLE_THIRD_PARTY_DIR}/install/mklml/include ${OpenCV_INCLUDE_DIRS} ) link_directories( ${PADDLE_LIB_DIR} ${PADDLE_THIRD_PARTY_DIR}/install/mkldnn/lib ${PADDLE_THIRD_PARTY_DIR}/install/mklml/lib ) add_executable(ocr_deploy src/main.cpp) target_link_libraries(ocr_deploy paddle_inference ${OpenCV_LIBS} )CMake相关的目录设置尽量用${CMAKE_CURRENT_SOURCE_DIR}拼路径不要写死绝对路径不然换机器又要改一堆配置。paddle_inference这个库在Windows下对应的就是paddle_inference.lib链接时库目录已经加过了直接写库名就行。还有一个很多教程不太提的点Paddle推理库解压后paddle/lib目录下其实只有DLL真正的导入库paddle_inference.lib也在里面但编译时它还会依赖third_party里mkldnn和mklml的库。第三方的include和lib路径都要加否则运行时会提示找不到mkldnn.dll或者编制报错找不到头文件。2.3 编译链接的架构一致性问题架构一致性是个很折磨人的细节。整个链条都必须是x64推理库是x64的OpenCV是x64的VS2017的解决方案平台也得是x64。CMake生成VS工程时需要在命令行指定架构参数cmake -S . -B build -G Visual Studio 15 2017 -A x64有些教程用老写法-G Visual Studio 15 2017 Win64效果一样但新写法更清晰。生成之后用VS2017打开build目录下的sln文件确认解决方案平台显示的是x64然后选择Release模式编译。编译时建议直接命令行cmake --build build --config Release这样干净利落不用来回在VS界面里点。如果编译通过build目录下Release文件夹里会出现ocr_deploy.exe。注意所有DLL都不会自动复制到exe旁边需要手动操作。3. C推理全流程拆解3.1 初始化推理引擎与模型加载Paddle Inference在C侧的API设计得还算清晰核心类是paddle_infer::Config和paddle_infer::Predictor。三个模型需要三个Predictor实例彼此独立。初始化代码如下#include paddle_inference_api.h #include opencv2/opencv.hpp #include numeric using namespace paddle_infer; std::shared_ptrPredictor create_predictor( const std::string model_dir, int cpu_threads) { Config config; config.SetModel(model_dir /inference.pdmodel, model_dir /inference.pdiparams); config.EnableMKLDNN(); config.SetCpuMathLibraryNumThreads(cpu_threads); config.SwitchIrOptim(true); config.EnableMemoryOptim(); return CreatePredictor(config); }EnableMKLDNN是CPU推理的关键优化项开启后推理速度有一倍多的提升。SetCpuMathLibraryNumThreads设置的是MKL数学库的线程数一般设成CPU物理核数或者物理核数减一别贪多。EnableMemoryOptim是开启显存/内存复用能有效降低推理过程的峰值内存。三个模型分别调用这个函数传入不同模型目录即可。Config在创建Predictor之前还可以调用DisableGlogInfo()来关闭推理库内部的日志输出不然每次初始化都会刷一堆调试信息影响日志可读性。3.2 图像预处理每一步都有讲究预处理是最容易出问题但同时又最容易被忽视的环节。我在第一次部署时识别结果全是乱码排查了大半天最后发现是通道顺序搞反了。PaddleOCR在训练时图像是RGB顺序输入的而OpenCV的imread默认读出来是BGR必须手动转换。完整预处理流程按模型分开处理。检测模型的输入是归一化到[0,1]的RGB图像尺寸按长边缩放到960具体值由模型决定官方默认det模型是960短边等比缩放并保证能被32整除。方向分类模型输入固定48x192。识别模型高度固定32宽度动态变化但为了batch推理方便通常会按比例缩放并pad到32的整数倍。核心预处理代码可以参考cv::Mat preprocess_for_det(const cv::Mat img, int resize_h, int resize_w) { cv::Mat rgb_img; cv::cvtColor(img, rgb_img, cv::COLOR_BGR2RGB); int w rgb_img.cols; int h rgb_img.rows; float ratio 960.0f / std::max(w, h); int rw static_castint(w * ratio); int rh static_castint(h * ratio); rw rw - rw % 32; rh rh - rh % 32; resize_h rh; resize_w rw; cv::Mat resized; cv::resize(rgb_img, resized, cv::Size(rw, rh)); resized.convertTo(resized, CV_32FC3, 1.0 / 255.0); return resized; }convertTo里的1.0/255.0就是归一化的缩放系数这个不能省。做完这些还需要把HWC格式转成CHW格式这是深度学习模型的通用输入格式。实现时直接遍历像素就行两张图都测试过纯C实现也就几毫秒的开销。识别和分类模型还需要额外的归一化参数均值mean是{0.485, 0.456, 0.406}标准差std是{0.229, 0.224, 0.225}。预处理时不但要除以255还要减去均值再除以标准差这一步直接影响识别准确率。检测模型只需要除以255不需要做均值归一化这个差异点非常容易混淆。3.3 推理执行、后处理与坐标还原输入模型之前要把预处理后的Mat数据拷贝到Input Tensor中。Tensor的shape要对应模型的输入要求比如检测模型是{1, 3, H, W}其中H和W就是预处理后resize出来的尺寸这是动态shape模型的好处宽高可变。代码如下auto input_names predictor-GetInputNames(); auto input_tensor predictor-GetInputHandle(input_names[0]); input_tensor-Reshape({1, 3, resize_h, resize_w}); input_tensor-CopyFromCpu(chw_data.data()); predictor-Run();运行完成之后获取输出Tensorauto output_names predictor-GetOutputNames(); auto output_tensor predictor-GetOutputHandle(output_names[0]); std::vectorint out_shape output_tensor-shape(); int numel std::accumulate(out_shape.begin(), out_shape.end(), 1, std::multipliesint()); std::vectorfloat out_data(numel); output_tensor-CopyToCpu(out_data.data());检测模型输出的是概率图shape是{1, 1, H, W}也就是每个像素属于文本框内部的概率。对这个概率图做阈值过滤概率大于0.3的像素视为文本区域。然后通过cv::findContours找连通域再对每个轮廓做最小外接矩形或多边形拟合就能得到文本框坐标。坐标是相对于resize后图像的要还原到原图直接除以缩放比例就行。分类模型输出的是{1, 2}的向量分别是“非文本旋转”和“文本旋转”的概率取最大概率的索引判断是否需要旋转180度。识别模型输出是{1, seq_len, class_num}的序列概率分布。每个时间步取最大概率索引再去掉重复字符和blank标记索引为0最后通过字典文件映射成文字。CTCLoss解码的逻辑不多但细节不少比如“apple”这类连续重复字符在CTC解码时会合并成一个“a”这个逻辑写不对所有识别结果都会出错。坐标还原还有一个容易忽略的点方向分类模型如果判断需要旋转那么原先的检测框坐标也要相应变换否则最终可视化时文本框和识别文本会错位。虽然这个边界情况不多但一定要处理。4. 避坑指南与性能调优4.1 高频报错速查表我把整个部署过程中遇到的高频问题整理成了表格每个问题都附上了排查思路。这张表建议收藏遇到问题先对照一遍。报错现象根本原因解决方法编译时大量LNK2019/LNK2001未解析符号推理库与VS版本不匹配或架构不是x64确认下载msvc2017版本确认工程为x64 Release运行时报找不到paddle_inference.dllexe运行时没找到库把paddle/lib下所有DLL复制到exe目录或加入PATH报错找不到mkldnn.dll或mklml.dll依赖第三方库DLL缺失从paddle/third_party对应目录复制DLLCV_32FC3输入导致输出全为0输入Tensor数据类型不匹配确认拷贝前数据是float32连续内存识别结果乱码或空白预处理通道顺序或归一化错误检查BGR转RGB、除以255、均值方差归一化检测框坐标大面积偏移坐标没有按缩放比例还原计算原图宽高与resize后宽高的比例并正确还原推理速度极慢未开启MKLDNN或线程数设置不合理调用EnableMKLDNN线程数设为物理核数模型加载时算子解析失败推理库版本太旧模型里有新算子换用匹配版本的推理库或重新导出老格式模型关于DLL拷贝这里有个小技巧paddle_inference的DLL还依赖third_party里的几个DLL光拷paddle/lib目录是不够的。最稳妥的办法是把paddle/lib、third_party/install/mkldnn/lib、third_party/install/mklml/lib里的所有DLL都复制到exe同级目录。用一段简单的批处理命令可以搞定copy third_party\paddle_inference\paddle\lib\*.dll build\Release\ copy third_party\paddle_inference\third_party\install\mkldnn\lib\*.dll build\Release\ copy third_party\paddle_inference\third_party\install\mklml\lib\*.dll build\Release\ copy third_party\opencv\x64\vc15\bin\*.dll build\Release\OpenCV的DLL路径根据版本不同会有一点差异有的在build/x64/vc15/bin有的在build/x64/vc14/bin要按实际解压路径调整。4.2 CPU推理性能调优方向CPU推理的性能优化有几个非常立竿见影的方向按优先级排列如下。第一开启MKLDNN。这个前面已经提过再强调一遍不开启的话推理速度掉一半以上。第二线程数设置。检测、分类、识别三个模型共用一个线程池如果每个Predictor都设置4线程三个模型串行推理时线程数其实够用。但要注意机器物理核数太少而线程数设置过大会导致上下文切换开销远超计算收益。第三图像尺寸控制。PaddleOCR的检测模型默认输入最长边是960如果你的业务场景是手机截图或者文档扫描图原图动辄两三千像素resize到960本身会丢失一些精度但速度提升是实打实的。可以根据业务场景微调这个值比如做手机截图识别性价比比较好的最长边是1280。第四识别模型的输入宽度直接影响速度。如果OCR目标都是固定宽度的内容比如车牌号或者单行文本可以固定识别宽度为320完全够用速度比动态宽度快不少。不过这个需要配合固定shape的模型导出默认下载的模型是动态shape实际推理时会按实际文本宽度计算。第五减少图像解码开销。如果图像来源是本地文件cv::imread的开销可以忽略如果来源是网络流或者相机帧解码就是瓶颈可以考虑用cv::imdecode配合理想的解码参数。实测下来在I5-10400这类中端CPU上开启全部优化之后一张1080p的文档图完整OCR流程大约在800ms到1.2秒之间基本满足大多数离线场景需求。如果想要更快可以考虑换PaddleOCR的mobile系列模型体积更小、速度更快精度损失在可接受范围内。4.3 关于模型和字典文件的一些补充识别模型推理出来的是类别索引必须通过字典文件映射成文字。PaddleOCR简体中文模型的字典文件是ppocr_keys_v1.txt里面按行排列所有字符。字典里第一行通常是一个空字符或者特殊符号对应CTC解码里的blank也就是索引0解码时遇到索引0要跳过这个和重复字符合并的逻辑是配套的。字典加载很简单就是按行读文件每一行的内容就是索引对应的字符std::vectorstd::string load_dict(const std::string dict_path) { std::vectorstd::string dict; std::ifstream fin(dict_path); std::string line; while (std::getline(fin, line)) { dict.push_back(line); } return dict; }注意文件编码PaddleOCR的字典是UTF-8编码读取时不要再做转码否则中文会乱码。Windows下如果发现识别输出的中文乱码而英文字符正常八成是这块出了问题。JSON输出和结果封装的问题也值得提前考虑。实际业务接入时建议把检测框坐标、识别文本、置信度封装成结构体或JSON字符串输出方便上层调用。一个小提醒std::ofstream输出UTF-8中文到Windows控制台时会乱码但写入文件没问题这是因为控制台默认代码页是GBK。如果你需要在控制台打印中文调试信息先调用SetConsoleOutputCP(CP_UTF8)设置一下代码页。5. 最后分享几点个人体会整套方案从下载依赖到推理跑通我花了大约两天时间。回头复盘最耗时的不是代码本身而是版本匹配和环境配置。Paddle Inference在Windows下的C部署已经相当成熟API设计也稳定但前提是版本要对、架构要统一、依赖要齐全。这三点做好了整个部署过程其实可以压缩到半天以内。如果是要接到自己的项目里建议把三个模型的Predictor实例放到一个管理器类里统一管理初始化一次后续每次调用只走预处理、推理、后处理的纯函数流程。这样既避免了重复创建Predictor的性能损耗也让代码结构更清晰。最后一个小建议不要把C推理的代码和业务逻辑硬耦合。OCR识别本质上是“给定一张图返回一串文本加坐标”的计算过程单独封装成一个模块上层不管是做文档扫描、车牌识别还是票据录入都能零成本接入。这套CPU版本的部署方案虽然性能不如GPU但在Windows桌面端场景下足够稳定生产环境完全可用。
返回列表